sdd-full 4.6.2 → 4.8.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.
Files changed (173) hide show
  1. package/bin.js +1 -1
  2. package/package.json +1 -1
  3. package/skills/.agents/skills/flutter-add-integration-test/SKILL.md +165 -0
  4. package/skills/.agents/skills/flutter-add-widget-preview/SKILL.md +147 -0
  5. package/skills/.agents/skills/flutter-add-widget-test/SKILL.md +156 -0
  6. package/skills/.agents/skills/flutter-apply-architecture-best-practices/SKILL.md +164 -0
  7. package/skills/.agents/skills/flutter-build-responsive-layout/SKILL.md +141 -0
  8. package/skills/.agents/skills/flutter-fix-layout-issues/SKILL.md +132 -0
  9. package/skills/.agents/skills/flutter-implement-json-serialization/SKILL.md +155 -0
  10. package/skills/.agents/skills/flutter-setup-declarative-routing/SKILL.md +257 -0
  11. package/skills/.agents/skills/flutter-setup-localization/SKILL.md +212 -0
  12. package/skills/.agents/skills/flutter-use-http-package/SKILL.md +177 -0
  13. package/skills/VERSION.md +176 -62
  14. package/skills/design-planning/ai-coding-rules/SKILL.md +5 -13
  15. package/skills/design-planning/design-to-code/SKILL.md +5 -14
  16. package/skills/design-planning/enterprise-spec/SKILL.md +5 -13
  17. package/skills/design-planning/flutter-av/SKILL.md +5 -16
  18. package/skills/design-planning/flutter-map/SKILL.md +5 -14
  19. package/skills/design-planning/function-sdd/SKILL.md +5 -13
  20. package/skills/design-planning/global-overlay-stack-standard/SKILL.md +73 -0
  21. package/skills/design-planning/ui-motion-interaction-standard/SKILL.md +69 -0
  22. package/skills/design-planning/ui-sdd-specialized/SKILL.md +5 -14
  23. package/skills/development-execution/flutter-errors/SKILL.md +5 -15
  24. package/skills/flutter-skills/.github/dependabot.yaml +15 -0
  25. package/skills/flutter-skills/.github/workflows/dart_skills_lint_workflow.yaml +68 -0
  26. package/skills/flutter-skills/.github/workflows/skills_tool.yaml +51 -0
  27. package/skills/flutter-skills/CODE_OF_CONDUCT.md +3 -0
  28. package/skills/flutter-skills/CONTRIBUTING.md +36 -0
  29. package/skills/flutter-skills/LICENSE +26 -0
  30. package/skills/flutter-skills/README.md +50 -0
  31. package/skills/flutter-skills/pubspec.yaml +9 -0
  32. package/skills/flutter-skills/resources/flutter_skills.yaml +434 -0
  33. package/skills/flutter-skills/skills/flutter-add-integration-test/SKILL.md +163 -0
  34. package/skills/flutter-skills/skills/flutter-add-widget-preview/SKILL.md +145 -0
  35. package/skills/flutter-skills/skills/flutter-add-widget-test/SKILL.md +154 -0
  36. package/skills/flutter-skills/skills/flutter-apply-architecture-best-practices/SKILL.md +162 -0
  37. package/skills/flutter-skills/skills/flutter-build-responsive-layout/SKILL.md +139 -0
  38. package/skills/flutter-skills/skills/flutter-fix-layout-issues/SKILL.md +130 -0
  39. package/skills/flutter-skills/skills/flutter-implement-json-serialization/SKILL.md +153 -0
  40. package/skills/flutter-skills/skills/flutter-setup-declarative-routing/SKILL.md +255 -0
  41. package/skills/flutter-skills/skills/flutter-setup-localization/SKILL.md +210 -0
  42. package/skills/flutter-skills/skills/flutter-use-http-package/SKILL.md +175 -0
  43. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md +196 -0
  44. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-best-practices/SKILL.md +65 -0
  45. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-checks-migration/SKILL.md +158 -0
  46. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-cli-app-best-practices/SKILL.md +168 -0
  47. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-doc-validation/SKILL.md +87 -0
  48. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-long-lines/SKILL.md +101 -0
  49. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-matcher-best-practices/SKILL.md +136 -0
  50. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-modern-features/SKILL.md +266 -0
  51. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-package-maintenance/SKILL.md +92 -0
  52. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/SKILL.md +92 -0
  53. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/lib/src/calculator.dart +7 -0
  54. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/pubspec.yaml +8 -0
  55. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/test/calculator_test.dart +11 -0
  56. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/interpret_coverage.dart +95 -0
  57. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/pubspec.yaml +6 -0
  58. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/test/interpret_coverage_test.dart +93 -0
  59. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-fundamentals/SKILL.md +173 -0
  60. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md +27 -0
  61. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json +3 -0
  62. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/grill-me/SKILL.md +10 -0
  63. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/ignore.json +3 -0
  64. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/test-driven-development/SKILL.md +371 -0
  65. package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/test-driven-development/testing-anti-patterns.md +299 -0
  66. package/skills/flutter-skills/tool/dart_skills_lint/AUTHORS +7 -0
  67. package/skills/flutter-skills/tool/dart_skills_lint/CHANGELOG.md +12 -0
  68. package/skills/flutter-skills/tool/dart_skills_lint/CONTRIBUTING.md +51 -0
  69. package/skills/flutter-skills/tool/dart_skills_lint/LICENSE +27 -0
  70. package/skills/flutter-skills/tool/dart_skills_lint/README.md +203 -0
  71. package/skills/flutter-skills/tool/dart_skills_lint/analysis_options.yaml +296 -0
  72. package/skills/flutter-skills/tool/dart_skills_lint/bench/README.md +23 -0
  73. package/skills/flutter-skills/tool/dart_skills_lint/bench/baseline_throughput.dart +230 -0
  74. package/skills/flutter-skills/tool/dart_skills_lint/bin/cli.dart +10 -0
  75. package/skills/flutter-skills/tool/dart_skills_lint/dart_skills_lint.yaml +14 -0
  76. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/PRODUCTION_READYNESS.md +48 -0
  77. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/completion_migration_plan.md +99 -0
  78. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/legacy_patterns_report.md +110 -0
  79. package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/pub_vs_skill_report.md +56 -0
  80. package/skills/flutter-skills/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md +79 -0
  81. package/skills/flutter-skills/tool/dart_skills_lint/documentation/knowledge/architecture_overview.md +64 -0
  82. package/skills/flutter-skills/tool/dart_skills_lint/lib/dart_skills_lint.dart +11 -0
  83. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/config_parser.dart +156 -0
  84. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/entry_point.dart +354 -0
  85. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/fixable_rule.dart +20 -0
  86. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/analysis_severity.dart +15 -0
  87. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/check_type.dart +17 -0
  88. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/ignore_entry.dart +34 -0
  89. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/ignore_entry.g.dart +19 -0
  90. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skill_context.dart +27 -0
  91. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skill_rule.dart +27 -0
  92. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skills_ignores.dart +26 -0
  93. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skills_ignores.g.dart +24 -0
  94. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/validation_error.dart +31 -0
  95. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rule_registry.dart +79 -0
  96. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/absolute_paths_rule.dart +74 -0
  97. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/description_length_rule.dart +49 -0
  98. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/disallowed_field_rule.dart +61 -0
  99. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/name_format_rule.dart +167 -0
  100. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/relative_paths_rule.dart +72 -0
  101. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart +93 -0
  102. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/valid_yaml_metadata_rule.dart +74 -0
  103. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/skills_ignores_storage.dart +36 -0
  104. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/validation_session.dart +559 -0
  105. package/skills/flutter-skills/tool/dart_skills_lint/lib/src/validator.dart +238 -0
  106. package/skills/flutter-skills/tool/dart_skills_lint/pubspec.yaml +28 -0
  107. package/skills/flutter-skills/tool/dart_skills_lint/skills/README.md +10 -0
  108. package/skills/flutter-skills/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md +195 -0
  109. package/skills/flutter-skills/tool/dart_skills_lint/skills-lock.json +75 -0
  110. package/skills/flutter-skills/tool/dart_skills_lint/test/absolute_paths_test.dart +167 -0
  111. package/skills/flutter-skills/tool/dart_skills_lint/test/cli_integration_test.dart +683 -0
  112. package/skills/flutter-skills/tool/dart_skills_lint/test/config_file_test.dart +292 -0
  113. package/skills/flutter-skills/tool/dart_skills_lint/test/custom_rule_test.dart +122 -0
  114. package/skills/flutter-skills/tool/dart_skills_lint/test/directory_structure_test.dart +163 -0
  115. package/skills/flutter-skills/tool/dart_skills_lint/test/field_constraints_test.dart +178 -0
  116. package/skills/flutter-skills/tool/dart_skills_lint/test/fixer_test.dart +172 -0
  117. package/skills/flutter-skills/tool/dart_skills_lint/test/ignore_models_test.dart +63 -0
  118. package/skills/flutter-skills/tool/dart_skills_lint/test/metadata_validation_test.dart +116 -0
  119. package/skills/flutter-skills/tool/dart_skills_lint/test/relative_path_flag_test.dart +70 -0
  120. package/skills/flutter-skills/tool/dart_skills_lint/test/relative_paths_test.dart +172 -0
  121. package/skills/flutter-skills/tool/dart_skills_lint/test/resolve_rules_test.dart +82 -0
  122. package/skills/flutter-skills/tool/dart_skills_lint/test/rule_naming_test.dart +29 -0
  123. package/skills/flutter-skills/tool/dart_skills_lint/test/skills_ignores_storage_test.dart +89 -0
  124. package/skills/flutter-skills/tool/dart_skills_lint/test/test_utils.dart +19 -0
  125. package/skills/flutter-skills/tool/dart_skills_lint/test/trailing_whitespace_test.dart +152 -0
  126. package/skills/flutter-skills/tool/generator/README.md +150 -0
  127. package/skills/flutter-skills/tool/generator/analysis_options.yaml +143 -0
  128. package/skills/flutter-skills/tool/generator/bin/skills.dart +73 -0
  129. package/skills/flutter-skills/tool/generator/lib/src/commands/base_skill_command.dart +87 -0
  130. package/skills/flutter-skills/tool/generator/lib/src/commands/base_yaml_command.dart +83 -0
  131. package/skills/flutter-skills/tool/generator/lib/src/commands/generate_skill_command.dart +92 -0
  132. package/skills/flutter-skills/tool/generator/lib/src/commands/update_readme_command.dart +150 -0
  133. package/skills/flutter-skills/tool/generator/lib/src/commands/update_skill_command.dart +97 -0
  134. package/skills/flutter-skills/tool/generator/lib/src/commands/validate_skill_command.dart +284 -0
  135. package/skills/flutter-skills/tool/generator/lib/src/models/skill_params.dart +41 -0
  136. package/skills/flutter-skills/tool/generator/lib/src/services/gemini_service.dart +310 -0
  137. package/skills/flutter-skills/tool/generator/lib/src/services/markdown_converter.dart +226 -0
  138. package/skills/flutter-skills/tool/generator/lib/src/services/prompts.dart +72 -0
  139. package/skills/flutter-skills/tool/generator/lib/src/services/resource_fetcher_service.dart +84 -0
  140. package/skills/flutter-skills/tool/generator/lib/src/services/skill_instructions.dart +30 -0
  141. package/skills/flutter-skills/tool/generator/pubspec.yaml +32 -0
  142. package/skills/flutter-skills/tool/generator/test/commands/base_skill_command_test.dart +131 -0
  143. package/skills/flutter-skills/tool/generator/test/commands/validate_skills_input_test.dart +263 -0
  144. package/skills/flutter-skills/tool/generator/test/custom_skill_rules/last_modified_rule.dart +32 -0
  145. package/skills/flutter-skills/tool/generator/test/generate_skills_retry_test.dart +105 -0
  146. package/skills/flutter-skills/tool/generator/test/generate_skills_test.dart +519 -0
  147. package/skills/flutter-skills/tool/generator/test/lint_skills_test.dart +34 -0
  148. package/skills/flutter-skills/tool/generator/test/markdown_converter_test.dart +103 -0
  149. package/skills/flutter-skills/tool/generator/test/markdown_table_test.dart +131 -0
  150. package/skills/flutter-skills/tool/generator/test/models/skill_params_test.dart +37 -0
  151. package/skills/flutter-skills/tool/generator/test/services/gemini_service_test.dart +291 -0
  152. package/skills/flutter-skills/tool/generator/test/services/markdown_converter_test.dart +156 -0
  153. package/skills/flutter-skills/tool/generator/test/services/resource_fetcher_service_test.dart +188 -0
  154. package/skills/flutter-skills/tool/generator/test/update_skills_test.dart +241 -0
  155. package/skills/flutter-skills/tool/generator/test/validate_skills_test.dart +728 -0
  156. package/skills/quality-assurance/bdd-acceptance/SKILL.md +5 -14
  157. package/skills/quality-assurance/flutter-test/SKILL.md +5 -16
  158. package/skills/rules/project_rules.md +538 -127
  159. package/skills/special-tools/env-check/SKILL.md +5 -13
  160. package/skills/special-tools/ios-full-auto-debug/SKILL.md +5 -15
  161. package/skills/writing-skills/SKILL.md +654 -0
  162. package/skills/writing-skills/anthropic-best-practices.md +1149 -0
  163. package/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
  164. package/skills/writing-skills/graphviz-conventions.dot +172 -0
  165. package/skills/writing-skills/persuasion-principles.md +187 -0
  166. package/skills/writing-skills/render-graphs.js +168 -0
  167. package/skills/writing-skills/testing-skills-with-subagents.md +384 -0
  168. package/skills/checklist.md +0 -154
  169. package/skills/rules/user_rules.md +0 -263
  170. package/skills//345/256/214/346/225/264/345/274/200/345/217/221/346/265/201/347/250/213/346/211/213/345/206/214.md +0 -454
  171. package/skills//346/212/200/350/203/275/344/275/223/347/263/273/345/256/214/345/226/204/345/273/272/350/256/256.md +0 -308
  172. package/skills//346/212/200/350/203/275/344/275/277/347/224/250/346/214/207/345/215/227.md +0 -309
  173. package/skills//346/212/200/350/203/275/345/206/263/347/255/226/346/240/221.md +0 -338
@@ -0,0 +1,310 @@
1
+ // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2
+ // for details. All rights reserved. Use of this source code is governed by a
3
+ // BSD-style license that can be found in the LICENSE file.
4
+
5
+ import 'dart:io' as io;
6
+
7
+ import 'package:google_cloud_ai_generativelanguage_v1beta/generativelanguage.dart';
8
+ import 'package:http/http.dart' as http;
9
+ import 'package:logging/logging.dart';
10
+ import 'package:meta/meta.dart';
11
+ import 'package:retry/retry.dart';
12
+
13
+ import 'prompts.dart';
14
+ import 'skill_instructions.dart';
15
+
16
+ /// Service for interacting with the Gemini API to generate and validate skills.
17
+ class GeminiService {
18
+ /// Creates a new [GeminiService].
19
+ GeminiService({
20
+ required String apiKey,
21
+ http.Client? httpClient,
22
+ String? model,
23
+ }) : _model = model ?? defaultModel,
24
+ _client = _ApiKeyClient(httpClient ?? http.Client(), apiKey);
25
+
26
+ /// 0.2 is a good temperature for technical material.
27
+ static const double defaultTemperature = 0.2;
28
+
29
+ /// The default model to use for generation.
30
+ static const String defaultModel = 'models/gemini-3.1-pro-preview';
31
+
32
+ /// The default token budget for thinking.
33
+ static const int defaultThinkingBudget = 4096;
34
+
35
+ /// The default max output tokens for generation.
36
+ static const int defaultMaxOutputTokens = 8192;
37
+
38
+ /// The default safety settings to use for generation.
39
+ static final List<SafetySetting> defaultSafetySettings = [
40
+ SafetySetting(
41
+ category: HarmCategory.harmCategoryDangerousContent,
42
+ threshold: SafetySetting_HarmBlockThreshold.blockOnlyHigh,
43
+ ),
44
+ SafetySetting(
45
+ category: HarmCategory.harmCategoryHateSpeech,
46
+ threshold: SafetySetting_HarmBlockThreshold.blockOnlyHigh,
47
+ ),
48
+ SafetySetting(
49
+ category: HarmCategory.harmCategoryHarassment,
50
+ threshold: SafetySetting_HarmBlockThreshold.blockOnlyHigh,
51
+ ),
52
+ SafetySetting(
53
+ category: HarmCategory.harmCategorySexuallyExplicit,
54
+ threshold: SafetySetting_HarmBlockThreshold.blockOnlyHigh,
55
+ ),
56
+ ];
57
+
58
+ final String _model;
59
+ final http.Client _client;
60
+ final Logger _logger = Logger('GeminiService');
61
+
62
+ /// Generates the content for a skill based on raw markdown input.
63
+ Future<String?> generateSkillContent(
64
+ String rawMarkdown,
65
+ String skillName,
66
+ String description, {
67
+ String? instructions,
68
+ int thinkingBudget = defaultThinkingBudget,
69
+ }) async {
70
+ final service = GenerativeService(client: _client);
71
+ final lastModified = io.HttpDate.format(DateTime.now());
72
+ final prompt = Prompts.createSkillPrompt(rawMarkdown, instructions);
73
+
74
+ final request = _createRequest(
75
+ prompt,
76
+ systemInstruction: skillInstructions,
77
+ thinkingBudget: thinkingBudget,
78
+ );
79
+
80
+ _logger.info(
81
+ ' Model: $_model, Max Output Tokens: $defaultMaxOutputTokens, Thinking Budget: $thinkingBudget',
82
+ );
83
+
84
+ try {
85
+ const r = RetryOptions(maxAttempts: 3);
86
+ final response = await r.retry(() async {
87
+ final res = await service.generateContent(request);
88
+ final text = res.candidates.first.content?.parts
89
+ .where((part) => !part.thought)
90
+ .map((part) => part.text)
91
+ .where((text) => text != null)
92
+ .join('\n');
93
+
94
+ if (text == null || text.isEmpty) {
95
+ throw const FormatException('Empty response from Gemini');
96
+ }
97
+
98
+ return text;
99
+ }, onRetry: (e) => _logger.warning('Retrying Gemini generation: $e'));
100
+
101
+ final content = response;
102
+
103
+ final frontmatter =
104
+ '''---
105
+ name: $skillName
106
+ description: $description
107
+ metadata:
108
+ model: $_model
109
+ last_modified: $lastModified
110
+ ---
111
+ ''';
112
+
113
+ return frontmatter + (cleanContent(content) ?? '');
114
+ } on Object catch (e) {
115
+ _logger.severe('Gemini generation failed: $e');
116
+ return null;
117
+ }
118
+ }
119
+
120
+ /// Updates the content for a skill based on raw markdown input and existing content.
121
+ Future<String?> updateSkillContent(
122
+ String existingContent,
123
+ String rawMarkdown,
124
+ String skillName,
125
+ String description, {
126
+ String? instructions,
127
+ int thinkingBudget = defaultThinkingBudget,
128
+ }) async {
129
+ final service = GenerativeService(client: _client);
130
+ final lastModified = io.HttpDate.format(DateTime.now());
131
+ final prompt = Prompts.updateSkillPrompt(
132
+ existingContent,
133
+ rawMarkdown,
134
+ instructions,
135
+ );
136
+
137
+ final request = _createRequest(
138
+ prompt,
139
+ systemInstruction: skillInstructions,
140
+ thinkingBudget: thinkingBudget,
141
+ );
142
+
143
+ _logger.info(
144
+ ' Model: $_model, Max Output Tokens: $defaultMaxOutputTokens, Thinking Budget: $thinkingBudget',
145
+ );
146
+
147
+ try {
148
+ const r = RetryOptions(maxAttempts: 3);
149
+ final response = await r.retry(() async {
150
+ final res = await service.generateContent(request);
151
+ final text = res.candidates.first.content?.parts
152
+ .where((part) => !part.thought)
153
+ .map((part) => part.text)
154
+ .where((text) => text != null)
155
+ .join('\n');
156
+
157
+ if (text == null || text.isEmpty) {
158
+ throw const FormatException('Empty response from Gemini');
159
+ }
160
+
161
+ return text;
162
+ }, onRetry: (e) => _logger.warning('Retrying Gemini generation: $e'));
163
+
164
+ final content = response;
165
+
166
+ final frontmatter =
167
+ '''---
168
+ name: $skillName
169
+ description: $description
170
+ metadata:
171
+ model: $_model
172
+ last_modified: $lastModified
173
+ ---
174
+ ''';
175
+
176
+ return frontmatter + (cleanContent(content) ?? '');
177
+ } on Object catch (e) {
178
+ _logger.severe('Gemini update failed: $e');
179
+ return null;
180
+ }
181
+ }
182
+
183
+ /// Validates an existing skill
184
+ Future<String?> validateExistingSkillContent(
185
+ String markdown,
186
+ String skillName,
187
+ String instructions,
188
+ String generationDate,
189
+ String modelName,
190
+ String currentSkillContent, {
191
+ int thinkingBudget = defaultThinkingBudget,
192
+ }) async {
193
+ final service = GenerativeService(client: _client);
194
+ final validationPrompt = Prompts.validateExistingSkillContentPrompt(
195
+ markdown,
196
+ instructions,
197
+ generationDate,
198
+ modelName,
199
+ currentSkillContent,
200
+ );
201
+
202
+ final request = _createRequest(
203
+ validationPrompt,
204
+ systemInstruction: skillInstructions,
205
+ thinkingBudget: thinkingBudget,
206
+ );
207
+
208
+ _logger.info(
209
+ ' Model: $_model, Max Output Tokens: $defaultMaxOutputTokens, Thinking Budget: $thinkingBudget',
210
+ );
211
+
212
+ try {
213
+ const r = RetryOptions(maxAttempts: 3);
214
+ final response = await r.retry(() async {
215
+ final res = await service.generateContent(request);
216
+ final text = res.candidates.first.content?.parts
217
+ .where((part) => !part.thought)
218
+ .map((part) => part.text)
219
+ .where((text) => text != null)
220
+ .join('\n');
221
+
222
+ if (text == null || text.isEmpty) {
223
+ throw const FormatException('Empty response from Gemini');
224
+ }
225
+
226
+ return text;
227
+ }, onRetry: (e) => _logger.warning('Retrying Gemini validation: $e'));
228
+
229
+ return response;
230
+ } on Object catch (e) {
231
+ _logger.severe('Gemini validation failed: $e');
232
+ return null;
233
+ }
234
+ }
235
+
236
+ /// Cleans the generated content by removing markdown code blocks and frontmatter.
237
+ @visibleForTesting
238
+ String? cleanContent(String? content) {
239
+ if (content == null) return null;
240
+ var cleaned = content;
241
+ final startMatch = RegExp(
242
+ r'^\s*```[a-zA-Z]*\s*\n',
243
+ caseSensitive: false,
244
+ ).firstMatch(cleaned);
245
+ if (startMatch != null) {
246
+ cleaned = cleaned.substring(startMatch.end);
247
+ // Remove the last triple backticks if they exist
248
+ cleaned = cleaned.replaceAll(RegExp(r'\n```\s*$'), '');
249
+ }
250
+
251
+ final yamlStartIndex = cleaned.indexOf('---');
252
+ if (yamlStartIndex == 0) {
253
+ // Possible frontmatter, skip it
254
+ final end = cleaned.indexOf('---', 3);
255
+ if (end != -1) {
256
+ cleaned = cleaned.substring(end + 3).trim();
257
+ }
258
+ } else if (yamlStartIndex > 0) {
259
+ // Maybe noise before frontmatter, try to strip it if it looks like frontmatter
260
+ final end = cleaned.indexOf('---', yamlStartIndex + 3);
261
+ if (end != -1) {
262
+ cleaned = cleaned.substring(end + 3).trim();
263
+ }
264
+ }
265
+
266
+ // Ensure one trailing newline
267
+ return '${cleaned.trim()}\n';
268
+ }
269
+
270
+ GenerateContentRequest _createRequest(
271
+ String prompt, {
272
+ String? systemInstruction,
273
+ int thinkingBudget = defaultThinkingBudget,
274
+ }) {
275
+ return GenerateContentRequest(
276
+ model: _model,
277
+ systemInstruction: systemInstruction != null
278
+ ? Content(parts: [Part(text: systemInstruction)])
279
+ : null,
280
+ contents: [
281
+ Content(parts: [Part(text: prompt)]),
282
+ ],
283
+ // See [GenerationConfig] in package:google_cloud_ai_generativelanguage_v1beta
284
+ generationConfig: GenerationConfig(
285
+ temperature: defaultTemperature,
286
+ maxOutputTokens: defaultMaxOutputTokens,
287
+ thinkingConfig: thinkingBudget > 0
288
+ ? ThinkingConfig(
289
+ includeThoughts: true,
290
+ thinkingBudget: thinkingBudget,
291
+ )
292
+ : null,
293
+ ),
294
+ safetySettings: defaultSafetySettings,
295
+ );
296
+ }
297
+ }
298
+
299
+ class _ApiKeyClient extends http.BaseClient {
300
+ _ApiKeyClient(this._inner, this._apiKey);
301
+
302
+ final http.Client _inner;
303
+ final String _apiKey;
304
+
305
+ @override
306
+ Future<http.StreamedResponse> send(http.BaseRequest request) {
307
+ request.headers['x-goog-api-key'] = _apiKey;
308
+ return _inner.send(request);
309
+ }
310
+ }
@@ -0,0 +1,226 @@
1
+ // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2
+ // for details. All rights reserved. Use of this source code is governed by a
3
+ // BSD-style license that can be found in the LICENSE file.
4
+
5
+ import 'package:html/dom.dart';
6
+ import 'package:html/parser.dart';
7
+
8
+ /// Converts HTML content to Markdown.
9
+ class MarkdownConverter {
10
+ /// Converts HTML content to Markdown.
11
+ String convert(String htmlContent) {
12
+ final document = parse(htmlContent);
13
+ final body = document.body;
14
+ if (body == null) return '';
15
+ return _convertElement(body).trim();
16
+ }
17
+
18
+ String _convertElement(Element element) {
19
+ final buffer = StringBuffer();
20
+
21
+ for (final node in element.nodes) {
22
+ if (node is Text) {
23
+ buffer.write(node.text);
24
+ } else if (node is Element) {
25
+ buffer.write(_processTag(node));
26
+ }
27
+ }
28
+
29
+ return buffer.toString();
30
+ }
31
+
32
+ String _processTag(Element element) {
33
+ final content = _convertElement(element);
34
+
35
+ switch (element.localName) {
36
+ case 'h1':
37
+ return '\n# $content\n\n';
38
+ case 'h2':
39
+ return '\n## $content\n\n';
40
+ case 'h3':
41
+ return '\n### $content\n\n';
42
+ case 'h4':
43
+ return '\n#### $content\n\n';
44
+ case 'h5':
45
+ return '\n##### $content\n\n';
46
+ case 'h6':
47
+ return '\n###### $content\n\n';
48
+ case 'p':
49
+ return '$content\n\n';
50
+ case 'a':
51
+ final href = element.attributes['href'];
52
+ return '[$content]($href)';
53
+ case 'strong':
54
+ case 'b':
55
+ return '**$content**';
56
+ case 'em':
57
+ case 'i':
58
+ return '*$content*';
59
+ case 'del':
60
+ case 's':
61
+ case 'strike':
62
+ return '~~$content~~';
63
+ case 'code':
64
+ return '`$content`';
65
+ case 'pre':
66
+ return '\n```\n${element.text}\n```\n\n';
67
+ case 'blockquote':
68
+ return '\n> $content\n\n';
69
+ case 'hr':
70
+ return '\n---\n\n';
71
+ case 'ul':
72
+ return '\n$content\n';
73
+ case 'ol':
74
+ return '\n$content\n';
75
+ case 'li':
76
+ return '- $content\n';
77
+ case 'img':
78
+ final src = element.attributes['src'] ?? '';
79
+ final alt = element.attributes['alt'] ?? '';
80
+ return '![$alt]($src)';
81
+ case 'video':
82
+ final src = element.attributes['src'] ?? '';
83
+ final poster = element.attributes['poster'] ?? '';
84
+ final title = element.attributes['title'] ?? 'Video';
85
+
86
+ String? videoUrl;
87
+ if (src.isNotEmpty) {
88
+ videoUrl = src;
89
+ } else {
90
+ // Fallback for source elements
91
+ videoUrl = element.children
92
+ .where((e) => e.localName == 'source')
93
+ .map((e) => e.attributes['src'])
94
+ .firstWhere((s) => s != null, orElse: () => null);
95
+ }
96
+
97
+ if (videoUrl != null) {
98
+ if (poster.isNotEmpty) {
99
+ return '[![$title]($poster)]($videoUrl)';
100
+ }
101
+ return '[$title]($videoUrl)';
102
+ }
103
+ return '';
104
+ case 'iframe':
105
+ final src = element.attributes['src'] ?? '';
106
+ final title = element.attributes['title'] ?? 'Iframe';
107
+ if (src.isNotEmpty) {
108
+ return '[$title]($src)';
109
+ }
110
+ return '';
111
+ case 'table':
112
+ return _processTable(element);
113
+ case 'dl':
114
+ return _processDefinitionList(element);
115
+ case 'dt':
116
+ return '\n**$content**\n';
117
+ case 'dd':
118
+ return ': $content\n';
119
+ case 'details':
120
+ // Preserve details as HTML, but convert children to markdown?
121
+ // Or just preserve the tag structure and convert internal content.
122
+ // Let's try to preserve the tag but convert content.
123
+ return '\n<details>\n$content\n</details>\n';
124
+ case 'summary':
125
+ return '<summary>$content</summary>';
126
+ case 'br':
127
+ return '\n';
128
+ case 'div':
129
+ case 'section':
130
+ case 'main':
131
+ case 'article':
132
+ return '$content\n';
133
+ default:
134
+ return content;
135
+ }
136
+ }
137
+
138
+ String _processTable(Element table) {
139
+ // Simple table converter
140
+ // 1. Find headers (th)
141
+ // 2. Find rows (tr)
142
+ // 3. Construct markdown table
143
+
144
+ final buffer = StringBuffer('\n');
145
+ var headerCells = <Element>[];
146
+ final bodyRows = <Element>[];
147
+
148
+ final theads = table.children.where((e) => e.localName == 'thead').toList();
149
+ if (theads.isNotEmpty) {
150
+ final headerRows = theads.first.children
151
+ .where((e) => e.localName == 'tr')
152
+ .toList();
153
+ if (headerRows.isNotEmpty) {
154
+ final headerRow = headerRows.first;
155
+ headerCells = headerRow.children
156
+ .where((e) => e.localName == 'th')
157
+ .toList();
158
+ if (headerCells.isEmpty) {
159
+ headerCells = headerRow.children
160
+ .where((e) => e.localName == 'td')
161
+ .toList();
162
+ }
163
+ }
164
+ }
165
+
166
+ for (final child in table.children) {
167
+ if (child.localName == 'tbody' || child.localName == 'tfoot') {
168
+ bodyRows.addAll(child.children.where((e) => e.localName == 'tr'));
169
+ } else if (child.localName == 'tr') {
170
+ bodyRows.add(child);
171
+ }
172
+ }
173
+
174
+ // Promote first row to header if needed
175
+ if (headerCells.isEmpty && bodyRows.isNotEmpty) {
176
+ final firstRow = bodyRows.first;
177
+ headerCells = firstRow.children
178
+ .where((e) => e.localName == 'th')
179
+ .toList();
180
+ if (headerCells.isEmpty) {
181
+ headerCells = firstRow.children
182
+ .where((e) => e.localName == 'td')
183
+ .toList();
184
+ }
185
+ bodyRows.removeAt(0);
186
+ }
187
+
188
+ if (headerCells.isEmpty && bodyRows.isEmpty) return '';
189
+
190
+ // Write Header
191
+ buffer.write('|');
192
+ for (final cell in headerCells) {
193
+ buffer.write(' ${_convertElement(cell).trim()} |');
194
+ }
195
+ buffer.write('\n|');
196
+ for (var i = 0; i < headerCells.length; i++) {
197
+ buffer.write('---|');
198
+ }
199
+ buffer.write('\n');
200
+
201
+ // Write Body
202
+ for (final row in bodyRows) {
203
+ final cells = row.children
204
+ .where((e) => e.localName == 'td' || e.localName == 'th')
205
+ .toList();
206
+ if (cells.isEmpty) continue;
207
+ buffer.write('|');
208
+ for (final cell in cells) {
209
+ buffer.write(' ${_convertElement(cell).trim()} |');
210
+ }
211
+ buffer.write('\n');
212
+ }
213
+ buffer.write('\n');
214
+
215
+ return buffer.toString();
216
+ }
217
+
218
+ String _processDefinitionList(Element dl) {
219
+ final buffer = StringBuffer('\n');
220
+ for (final child in dl.children) {
221
+ buffer.write(_processTag(child));
222
+ }
223
+ buffer.write('\n\n');
224
+ return buffer.toString();
225
+ }
226
+ }
@@ -0,0 +1,72 @@
1
+ // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2
+ // for details. All rights reserved. Use of this source code is governed by a
3
+ // BSD-style license that can be found in the LICENSE file.
4
+
5
+ // ignore_for_file: avoid_classes_with_only_static_members
6
+
7
+ /// Manages prompts used by the GeminiService.
8
+ class Prompts {
9
+ /// Creates the prompt for generating a new skill.
10
+ static String createSkillPrompt(String markdown, String? instructions) {
11
+ return '''
12
+ Rewrite the following technical documentation into a high-quality "SKILL.md" file.
13
+
14
+ ${instructions != null && instructions.isNotEmpty ? 'Special Instructions: $instructions' : ''}
15
+
16
+ Raw Content:
17
+ $markdown
18
+ ''';
19
+ }
20
+
21
+ /// Creates the prompt for updating an existing skill.
22
+ static String updateSkillPrompt(
23
+ String existingContent,
24
+ String markdown,
25
+ String? instructions,
26
+ ) {
27
+ return '''
28
+ Update the following existing "SKILL.md" file using the provided new technical documentation.
29
+ Carefully integrate the new information without losing valuable existing instructions or context.
30
+
31
+ ${instructions != null && instructions.isNotEmpty ? 'Special Instructions: $instructions' : ''}
32
+
33
+ Existing SKILL.md Content:
34
+ $existingContent
35
+
36
+ New Technical Documentation (Raw Content):
37
+ $markdown
38
+ ''';
39
+ }
40
+
41
+ /// Creates the prompt for validating an existing skill.
42
+ static String validateExistingSkillContentPrompt(
43
+ String markdown,
44
+ String instructions,
45
+ String generationDate,
46
+ String modelName,
47
+ String currentSkillContent,
48
+ ) {
49
+ return '''
50
+ Validate the following skill document against the provided source material and verify if it is valid.
51
+ Focus on accuracy, structure, and completeness based on the Source Material and the system instructions.
52
+
53
+ Context:
54
+ - The skill was originally generated on: $generationDate
55
+ - The current evaluation is using model: $modelName
56
+ - The instructions used to generate the skill were:
57
+ $instructions
58
+
59
+ Source Material:
60
+ $markdown
61
+
62
+ Current Skill Content:
63
+ "$currentSkillContent"
64
+ ---
65
+
66
+ Grade the current output based on the instructions and the comparison to current website content and instructions today.
67
+ Establish a conclusion on whether the new skill is valid or not.
68
+ Reasons for a good or bad quality grade should be provided including concepts such as missing content, different model used, more than a few months old, etc.
69
+ On the very last line, output "Grade: [0-100]" representing overall quality of the skill compared to the assumed value if it were generated again today.
70
+ ''';
71
+ }
72
+ }
@@ -0,0 +1,84 @@
1
+ // Copyright (c) 2026, the Dart project authors. Please see the AUTHORS file
2
+ // for details. All rights reserved. Use of this source code is governed by a
3
+ // BSD-style license that can be found in the LICENSE file.
4
+
5
+ import 'dart:io' as io;
6
+
7
+ import 'package:http/http.dart' as http;
8
+ import 'package:logging/logging.dart';
9
+ import 'package:path/path.dart' as p;
10
+
11
+ import 'markdown_converter.dart';
12
+
13
+ /// Fetches and converts content from diverse resources.
14
+ class ResourceFetcherService {
15
+ /// Creates a new [ResourceFetcherService].
16
+ ResourceFetcherService({
17
+ required http.Client httpClient,
18
+ required Logger logger,
19
+ }) : _httpClient = httpClient,
20
+ _logger = logger;
21
+
22
+ final http.Client _httpClient;
23
+ final Logger _logger;
24
+
25
+ /// Fetches and converts content from a list of resources.
26
+ ///
27
+ /// Throws an [Exception] if fetching any resource fails. This strict behavior
28
+ /// prevents wasting tokens on generating low-quality skills when
29
+ /// source material is missing.
30
+ Future<String> fetchAndConvertContent(
31
+ List<String> resources, {
32
+ io.Directory? configDir,
33
+ }) async {
34
+ final converter = MarkdownConverter();
35
+ final sb = StringBuffer();
36
+ for (final resource in resources) {
37
+ _logger.info(' Fetching $resource...');
38
+
39
+ if (resource.startsWith('http://')) {
40
+ throw Exception(
41
+ 'Insecure HTTP URL found: $resource. '
42
+ 'Only HTTPS URLs or relative file paths are allowed.',
43
+ );
44
+ }
45
+
46
+ if (resource.startsWith('https://')) {
47
+ final response = await _httpClient.get(Uri.parse(resource));
48
+ if (response.statusCode == 200) {
49
+ sb
50
+ ..writeln('--- Raw content from $resource ---')
51
+ ..writeln(converter.convert(response.body));
52
+ } else {
53
+ throw Exception(
54
+ 'Failed to fetch $resource: HTTP ${response.statusCode}. '
55
+ 'Failing fast to save Gemini tokens.',
56
+ );
57
+ }
58
+ } else {
59
+ if (configDir == null) {
60
+ throw Exception(
61
+ 'Relative resource "$resource" found, but no configuration '
62
+ 'directory was provided to resolve it.',
63
+ );
64
+ }
65
+ final file = io.File(p.join(configDir.path, resource));
66
+ if (!file.existsSync()) {
67
+ throw Exception('Local resource file not found: ${file.path}');
68
+ }
69
+
70
+ final String content;
71
+ try {
72
+ content = file.readAsStringSync();
73
+ } on io.FileSystemException {
74
+ throw Exception('Local resource file is not readable: ${file.path}');
75
+ }
76
+
77
+ sb
78
+ ..writeln('--- Raw content from $resource ---')
79
+ ..writeln(content);
80
+ }
81
+ }
82
+ return sb.toString();
83
+ }
84
+ }