@michaelbel/cuckcoder-mcp 1.6.12 → 1.6.14

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.
Files changed (40) hide show
  1. package/assets/agents/architect-auditor.md +182 -0
  2. package/assets/agents/bug-hunter.md +185 -0
  3. package/assets/agents/build-engineer.md +148 -0
  4. package/assets/agents/business-analyst.md +178 -0
  5. package/assets/agents/code-refine.md +150 -0
  6. package/assets/agents/code-reviewer.md +180 -0
  7. package/assets/agents/compose-builder.md +192 -0
  8. package/assets/agents/device-ui-tester.md +133 -0
  9. package/assets/agents/devops-expert.md +177 -0
  10. package/assets/agents/explorer.md +132 -0
  11. package/assets/agents/github-project-manager.md +203 -0
  12. package/assets/agents/guide-android-builder.md +81 -0
  13. package/assets/agents/guide-writer.md +63 -0
  14. package/assets/agents/kotlin-engineer.md +215 -0
  15. package/assets/agents/mechanical-operator.md +166 -0
  16. package/assets/agents/notion-project-manager.md +185 -0
  17. package/assets/agents/performance-reviewer.md +188 -0
  18. package/assets/agents/security-auditor.md +203 -0
  19. package/assets/agents/swift-engineer.md +223 -0
  20. package/assets/agents/swiftui-builder.md +236 -0
  21. package/assets/agents/tech-writer.md +214 -0
  22. package/assets/agents/ux-reviewer.md +235 -0
  23. package/assets/rules/kotlin-datetime.md +10 -0
  24. package/assets/rules/resource.md +7 -1
  25. package/assets/workflows/architecture-sweep.js +98 -0
  26. package/assets/workflows/business-feature-sweep.js +218 -0
  27. package/assets/workflows/full-review.js +190 -0
  28. package/assets/workflows/mvi-compliance-sweep.js +74 -0
  29. package/assets/workflows/redesign-sweep.js +152 -0
  30. package/assets/workflows/refactoring-sweep.js +199 -0
  31. package/assets/workflows/security-sweep.js +98 -0
  32. package/assets/workflows/task-batch-create.js +104 -0
  33. package/dist/search.js +38 -0
  34. package/dist/server.js +161 -2
  35. package/dist/source/bundled.js +78 -0
  36. package/dist/source/github-source.js +76 -0
  37. package/dist/validation.js +16 -0
  38. package/dist/workflow-meta.js +29 -0
  39. package/package.json +1 -1
  40. package/assets/rules/app-badging.md +0 -170
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { WorkflowError } from "../errors.js";
5
5
  import { parseFrontmatter } from "../frontmatter.js";
6
+ import { parseWorkflowMeta } from "../workflow-meta.js";
6
7
  /**
7
8
  * Directory the npm package ships rules/skills in. `scripts/copy-assets.ts` populates this
8
9
  * directory (from the repository's `rules/`/`skills/`) before every build/dev/test/pack, and
@@ -20,11 +21,15 @@ export class BundledSource {
20
21
  ref;
21
22
  rulesDir;
22
23
  skillsDir;
24
+ agentsDir;
25
+ workflowsDir;
23
26
  constructor(ref) {
24
27
  this.ref = ref;
25
28
  const base = assetsDir();
26
29
  this.rulesDir = join(base, "rules");
27
30
  this.skillsDir = join(base, "skills");
31
+ this.agentsDir = join(base, "agents");
32
+ this.workflowsDir = join(base, "workflows");
28
33
  }
29
34
  info() {
30
35
  return { kind: "bundled", ref: this.ref };
@@ -75,4 +80,77 @@ export class BundledSource {
75
80
  const { fields, body } = parseFrontmatter(raw);
76
81
  return { description: fields.description ?? "", content: body.trim() };
77
82
  }
83
+ async listAgents() {
84
+ const names = readdirSync(this.agentsDir, { withFileTypes: true })
85
+ .filter((entry) => entry.isFile() && /^[a-z0-9]+(?:-[a-z0-9]+)*\.md$/.test(entry.name))
86
+ .map((entry) => entry.name.replace(/\.md$/, ""))
87
+ .sort();
88
+ const summaries = [];
89
+ for (const name of names) {
90
+ try {
91
+ const raw = readFileSync(join(this.agentsDir, `${name}.md`), "utf8");
92
+ const { fields } = parseFrontmatter(raw);
93
+ summaries.push({ name, description: fields.description ?? "" });
94
+ }
95
+ catch {
96
+ // An agent file without readable frontmatter is not listed; get_agent will still
97
+ // surface NOT_FOUND if it's requested directly.
98
+ }
99
+ }
100
+ return summaries;
101
+ }
102
+ async getAgent(name) {
103
+ const path = join(this.agentsDir, `${name}.md`);
104
+ let raw;
105
+ try {
106
+ raw = readFileSync(path, "utf8");
107
+ }
108
+ catch {
109
+ throw new WorkflowError("NOT_FOUND", `Agent '${name}' was not found in the bundled snapshot.`);
110
+ }
111
+ const { fields, body } = parseFrontmatter(raw);
112
+ return {
113
+ description: fields.description ?? "",
114
+ tools: fields.tools ?? "",
115
+ disallowedTools: fields.disallowedTools ?? "",
116
+ content: body.trim(),
117
+ };
118
+ }
119
+ async listWorkflows() {
120
+ const names = readdirSync(this.workflowsDir, { withFileTypes: true })
121
+ .filter((entry) => entry.isFile() && /^[a-z0-9]+(?:-[a-z0-9]+)*\.js$/.test(entry.name))
122
+ .map((entry) => entry.name.replace(/\.js$/, ""))
123
+ .sort();
124
+ const summaries = [];
125
+ for (const name of names) {
126
+ try {
127
+ const raw = readFileSync(join(this.workflowsDir, `${name}.js`), "utf8");
128
+ const meta = parseWorkflowMeta(raw);
129
+ summaries.push({ name, description: meta.description });
130
+ }
131
+ catch {
132
+ // A workflow file whose meta block can't be parsed is not listed; get_workflow will
133
+ // still surface an error if it's requested directly.
134
+ }
135
+ }
136
+ return summaries;
137
+ }
138
+ async getWorkflow(name) {
139
+ const path = join(this.workflowsDir, `${name}.js`);
140
+ let raw;
141
+ try {
142
+ raw = readFileSync(path, "utf8");
143
+ }
144
+ catch {
145
+ throw new WorkflowError("NOT_FOUND", `Workflow '${name}' was not found in the bundled snapshot.`);
146
+ }
147
+ let meta;
148
+ try {
149
+ meta = parseWorkflowMeta(raw);
150
+ }
151
+ catch (error) {
152
+ throw new WorkflowError("INTERNAL_ERROR", `Workflow '${name}' has an unparsable meta block: ${error instanceof Error ? error.message : String(error)}`);
153
+ }
154
+ return { description: meta.description, whenToUse: meta.whenToUse, content: raw };
155
+ }
78
156
  }
@@ -1,5 +1,6 @@
1
1
  import { WorkflowError } from "../errors.js";
2
2
  import { parseFrontmatter } from "../frontmatter.js";
3
+ import { parseWorkflowMeta } from "../workflow-meta.js";
3
4
  import { GithubClient } from "./github.js";
4
5
  const OWNER = "michaelbel";
5
6
  const REPO = "cuckcoder";
@@ -65,4 +66,79 @@ export class GithubSource {
65
66
  const { fields, body } = parseFrontmatter(raw);
66
67
  return { description: fields.description ?? "", content: body.trim() };
67
68
  }
69
+ async listAgents() {
70
+ const tree = await this.client.listTree(this.ref);
71
+ const paths = tree
72
+ .filter((item) => item.type === "blob" && /^agents\/[a-z0-9]+(?:-[a-z0-9]+)*\.md$/.test(item.path))
73
+ .map((item) => item.path)
74
+ .sort();
75
+ const summaries = [];
76
+ for (const path of paths) {
77
+ const name = path.replace(/^agents\//, "").replace(/\.md$/, "");
78
+ const raw = await this.client.fetchFile(this.ref, path);
79
+ const { fields } = parseFrontmatter(raw);
80
+ summaries.push({ name, description: fields.description ?? "" });
81
+ }
82
+ return summaries;
83
+ }
84
+ async getAgent(name) {
85
+ let raw;
86
+ try {
87
+ raw = await this.client.fetchFile(this.ref, `agents/${name}.md`);
88
+ }
89
+ catch (error) {
90
+ if (error instanceof WorkflowError && error.code === "NOT_FOUND") {
91
+ throw new WorkflowError("NOT_FOUND", `Agent '${name}' was not found at ref '${this.ref}'.`);
92
+ }
93
+ throw error;
94
+ }
95
+ const { fields, body } = parseFrontmatter(raw);
96
+ return {
97
+ description: fields.description ?? "",
98
+ tools: fields.tools ?? "",
99
+ disallowedTools: fields.disallowedTools ?? "",
100
+ content: body.trim(),
101
+ };
102
+ }
103
+ async listWorkflows() {
104
+ const tree = await this.client.listTree(this.ref);
105
+ const paths = tree
106
+ .filter((item) => item.type === "blob" && /^workflows\/[a-z0-9]+(?:-[a-z0-9]+)*\.js$/.test(item.path))
107
+ .map((item) => item.path)
108
+ .sort();
109
+ const summaries = [];
110
+ for (const path of paths) {
111
+ const name = path.replace(/^workflows\//, "").replace(/\.js$/, "");
112
+ const raw = await this.client.fetchFile(this.ref, path);
113
+ try {
114
+ const meta = parseWorkflowMeta(raw);
115
+ summaries.push({ name, description: meta.description });
116
+ }
117
+ catch {
118
+ // A workflow file whose meta block can't be parsed is not listed; get_workflow will
119
+ // still surface an error if it's requested directly.
120
+ }
121
+ }
122
+ return summaries;
123
+ }
124
+ async getWorkflow(name) {
125
+ let raw;
126
+ try {
127
+ raw = await this.client.fetchFile(this.ref, `workflows/${name}.js`);
128
+ }
129
+ catch (error) {
130
+ if (error instanceof WorkflowError && error.code === "NOT_FOUND") {
131
+ throw new WorkflowError("NOT_FOUND", `Workflow '${name}' was not found at ref '${this.ref}'.`);
132
+ }
133
+ throw error;
134
+ }
135
+ let meta;
136
+ try {
137
+ meta = parseWorkflowMeta(raw);
138
+ }
139
+ catch (error) {
140
+ throw new WorkflowError("INTERNAL_ERROR", `Workflow '${name}' has an unparsable meta block: ${error instanceof Error ? error.message : String(error)}`);
141
+ }
142
+ return { description: meta.description, whenToUse: meta.whenToUse, content: raw };
143
+ }
68
144
  }
@@ -31,3 +31,19 @@ export function validateRuleName(rawName) {
31
31
  }
32
32
  return rawName;
33
33
  }
34
+ /** Agent name: the lowercase kebab-case basename without `.md`, e.g. `kotlin-engineer`. */
35
+ const AGENT_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
36
+ export function validateAgentName(rawName) {
37
+ if (!AGENT_NAME_PATTERN.test(rawName)) {
38
+ throw new WorkflowError("INVALID_NAME", `Invalid agent name '${rawName}'. Expected a lowercase kebab-case agent name, e.g. 'kotlin-engineer'.`);
39
+ }
40
+ return rawName;
41
+ }
42
+ /** Workflow name: the lowercase kebab-case basename without `.js`, e.g. `full-review`. */
43
+ const WORKFLOW_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
44
+ export function validateWorkflowName(rawName) {
45
+ if (!WORKFLOW_NAME_PATTERN.test(rawName)) {
46
+ throw new WorkflowError("INVALID_NAME", `Invalid workflow name '${rawName}'. Expected a lowercase kebab-case workflow name, e.g. 'full-review'.`);
47
+ }
48
+ return rawName;
49
+ }
@@ -0,0 +1,29 @@
1
+ const META_BLOCK_PATTERN = /export\s+const\s+meta\s*=\s*(\{[\s\S]*?\n\})/;
2
+ /**
3
+ * Extracts `export const meta = { name, description, whenToUse? }` from a workflow module's
4
+ * source text without importing the module. Workflow files reference sweep-runtime globals
5
+ * (`args`, `agent`, `phase`, ...) at the top level, so `import()`-ing one directly would throw;
6
+ * this only evaluates the isolated `meta` object literal, which in every workflow is just string
7
+ * literals joined with `+`.
8
+ */
9
+ export function parseWorkflowMeta(source) {
10
+ const match = source.match(META_BLOCK_PATTERN);
11
+ if (!match) {
12
+ throw new Error("no 'export const meta = {...}' block found");
13
+ }
14
+ let meta;
15
+ try {
16
+ meta = new Function(`"use strict"; return (${match[1]});`)();
17
+ }
18
+ catch (error) {
19
+ throw new Error(`failed to evaluate the workflow meta block: ${error instanceof Error ? error.message : String(error)}`);
20
+ }
21
+ if (typeof meta !== "object" ||
22
+ meta === null ||
23
+ typeof meta.name !== "string" ||
24
+ typeof meta.description !== "string") {
25
+ throw new Error("workflow meta block did not evaluate to { name: string, description: string, ... }");
26
+ }
27
+ const { name, description, whenToUse } = meta;
28
+ return { name, description, whenToUse: typeof whenToUse === "string" ? whenToUse : "" };
29
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaelbel/cuckcoder-mcp",
3
- "version": "1.6.12",
3
+ "version": "1.6.14",
4
4
  "description": "MCP server for Cuckcoder rules and skills",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -1,170 +0,0 @@
1
- ---
2
- description: >-
3
- Регресс-гард доступности устройств: aapt2 dump badging в golden-файл app/badging, обязательный
4
- check<Variant>Badging в CI, необязательное железо через android:required=false
5
- paths:
6
- - "**/*.gradle.kts"
7
- - "**/AndroidManifest.xml"
8
- ---
9
-
10
- `aapt2 dump badging` по собранному APK показывает эффективный набор `uses-feature` /
11
- `uses-permission` / `uses-sdk` — то, из-за чего Google Play скрывает приложение на планшетах,
12
- Android TV, Wear OS, складных устройствах и авто. Обновление зависимости может молча добавить
13
- `android:required` hardware feature (камера, телефония, автофокус) и срезать доступность, а в
14
- диффе исходников этого не видно. Гард фиксирует бейдж в golden-файл и валит CI при расхождении.
15
-
16
- ## Что заводится в проекте
17
-
18
- - Convention-плагин `build-logic/`, который на каждый application-вариант регистрирует три задачи:
19
- - `generate<Variant>Badging` — `aapt2 dump badging` по универсальному APK варианта → генерируемый
20
- файл в `build/`.
21
- - `update<Variant>Badging` — копирует генерируемый файл в golden `app/badging/<variant>.txt`
22
- (запускается локально, результат коммитится и проходит ревью).
23
- - `check<Variant>Badging` — сравнивает генерируемый с golden; при расхождении фейлит с диффом.
24
- - Golden-файлы `app/badging/*.txt` — под контролем версий; изменение бейджа всегда осознанное и
25
- видно в PR.
26
-
27
- ## Правила
28
-
29
- - `check<Variant>Badging` (как минимум для release-варианта) — обязательный шаг CI на каждый PR.
30
- - Изменение `app/badging/*.txt` в PR без соответствующей причины (новая фича, осознанное
31
- требование к железу) — стоп-фактор ревью.
32
- - Все `uses-feature` для необязательного железа объявляй с `android:required="false"`; жёсткое
33
- требование железа — только когда приложение без него нефункционально.
34
- - Не добавляй `check*Badging` в `dependsOn` обычной сборки — это отдельный verification-шаг, чтобы
35
- не замедлять локальный цикл.
36
-
37
- ## Скелет convention-плагина
38
-
39
- ```kotlin
40
- // build-logic/convention/src/main/kotlin/AndroidApplicationBadgingConventionPlugin.kt
41
- import com.android.build.api.artifact.SingleArtifact
42
- import com.android.build.api.variant.ApplicationAndroidComponentsExtension
43
- import org.gradle.api.DefaultTask
44
- import org.gradle.api.Plugin
45
- import org.gradle.api.Project
46
- import org.gradle.api.file.RegularFileProperty
47
- import org.gradle.api.provider.Property
48
- import org.gradle.api.tasks.CacheableTask
49
- import org.gradle.api.tasks.Copy
50
- import org.gradle.api.tasks.InputFile
51
- import org.gradle.api.tasks.OutputFile
52
- import org.gradle.api.tasks.PathSensitive
53
- import org.gradle.api.tasks.PathSensitivity
54
- import org.gradle.api.tasks.TaskAction
55
- import org.gradle.kotlin.dsl.register
56
-
57
- class AndroidApplicationBadgingConventionPlugin : Plugin<Project> {
58
- override fun apply(target: Project) {
59
- val components = target.extensions
60
- .getByType(ApplicationAndroidComponentsExtension::class.java)
61
-
62
- components.onVariants { variant ->
63
- val capitalised = variant.name.replaceFirstChar { it.uppercaseChar() }
64
- val golden = target.rootDir.resolve("app/badging/${variant.name}.txt")
65
- val generated = target.layout.buildDirectory.file("badging/${variant.name}.txt")
66
- val apk = variant.artifacts.get(SingleArtifact.APK)
67
-
68
- val generate = target.tasks.register<GenerateBadgingTask>("generate${capitalised}Badging") {
69
- apkDir.set(apk)
70
- aapt2.set(resolveAapt2(target))
71
- output.set(generated)
72
- }
73
-
74
- target.tasks.register<Copy>("update${capitalised}Badging") {
75
- group = "badging"
76
- description = "Обновляет app/badging/${variant.name}.txt — закоммить результат"
77
- dependsOn(generate)
78
- from(generated)
79
- into(golden.parentFile.also { it.mkdirs() })
80
- rename { golden.name }
81
- }
82
-
83
- target.tasks.register<CheckBadgingTask>("check${capitalised}Badging") {
84
- group = "verification"
85
- description = "Фейлит, если требуемые фичи ${variant.name}-APK разошлись с golden-файлом"
86
- dependsOn(generate)
87
- generated.set(generated)
88
- this.golden.set(golden)
89
- }
90
- }
91
- }
92
- }
93
-
94
- @CacheableTask
95
- abstract class GenerateBadgingTask : DefaultTask() {
96
- @get:InputFile @get:PathSensitive(PathSensitivity.NONE)
97
- abstract val apkDir: RegularFileProperty
98
-
99
- @get:org.gradle.api.tasks.Input
100
- abstract val aapt2: Property<String>
101
-
102
- @get:OutputFile
103
- abstract val output: RegularFileProperty
104
-
105
- @TaskAction
106
- fun run() {
107
- val apk = apkDir.get().asFile.parentFile
108
- .listFiles { f -> f.extension == "apk" }?.firstOrNull()
109
- ?: error("APK не найден в ${apkDir.get().asFile.parentFile}")
110
- val out = output.get().asFile.also { it.parentFile.mkdirs() }
111
- project.exec {
112
- commandLine(aapt2.get(), "dump", "badging", apk.absolutePath)
113
- standardOutput = out.outputStream()
114
- }
115
- }
116
- }
117
-
118
- @CacheableTask
119
- abstract class CheckBadgingTask : DefaultTask() {
120
- @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE)
121
- abstract val generated: RegularFileProperty
122
-
123
- @get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE)
124
- abstract val golden: RegularFileProperty
125
-
126
- @TaskAction
127
- fun run() {
128
- val g = generated.get().asFile.readText()
129
- val expected = golden.get().asFile.let {
130
- if (!it.exists()) error("Нет golden-файла ${it.path} — запусти update…Badging и закоммить")
131
- it.readText()
132
- }
133
- if (g.trim() != expected.trim()) {
134
- error(
135
- "Бейдж APK разошёлся с golden-файлом ${golden.get().asFile.path}.\n" +
136
- "Если изменение осознанное — запусти ./gradlew update…Badging и закоммить новый golden."
137
- )
138
- }
139
- }
140
- }
141
-
142
- private fun resolveAapt2(project: Project): String {
143
- val android = project.extensions
144
- .getByType(com.android.build.gradle.BaseExtension::class.java)
145
- return android.sdkDirectory
146
- .resolve("build-tools/${android.buildToolsVersion}/aapt2")
147
- .absolutePath
148
- }
149
- ```
150
-
151
- ## CI
152
-
153
- ```yaml
154
- # .github/workflows/pr-check.yml (фрагмент)
155
- - name: App badging check
156
- run: ./gradlew checkReleaseBadging
157
- ```
158
-
159
- ## Как выглядит golden-файл
160
-
161
- ```
162
- # app/badging/release.txt
163
- package: name='com.example.app' versionCode='142' versionName='1.4.2'
164
- sdkVersion:'26'
165
- targetSdkVersion:'35'
166
- uses-permission: name='android.permission.INTERNET'
167
- uses-feature-not-required: name='android.hardware.camera'
168
- feature-group: label=''
169
- uses-feature: name='android.hardware.faketouch'
170
- ```