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.
- package/bin.js +1 -1
- package/package.json +1 -1
- package/skills/.agents/skills/flutter-add-integration-test/SKILL.md +165 -0
- package/skills/.agents/skills/flutter-add-widget-preview/SKILL.md +147 -0
- package/skills/.agents/skills/flutter-add-widget-test/SKILL.md +156 -0
- package/skills/.agents/skills/flutter-apply-architecture-best-practices/SKILL.md +164 -0
- package/skills/.agents/skills/flutter-build-responsive-layout/SKILL.md +141 -0
- package/skills/.agents/skills/flutter-fix-layout-issues/SKILL.md +132 -0
- package/skills/.agents/skills/flutter-implement-json-serialization/SKILL.md +155 -0
- package/skills/.agents/skills/flutter-setup-declarative-routing/SKILL.md +257 -0
- package/skills/.agents/skills/flutter-setup-localization/SKILL.md +212 -0
- package/skills/.agents/skills/flutter-use-http-package/SKILL.md +177 -0
- package/skills/VERSION.md +176 -62
- package/skills/design-planning/ai-coding-rules/SKILL.md +5 -13
- package/skills/design-planning/design-to-code/SKILL.md +5 -14
- package/skills/design-planning/enterprise-spec/SKILL.md +5 -13
- package/skills/design-planning/flutter-av/SKILL.md +5 -16
- package/skills/design-planning/flutter-map/SKILL.md +5 -14
- package/skills/design-planning/function-sdd/SKILL.md +5 -13
- package/skills/design-planning/global-overlay-stack-standard/SKILL.md +73 -0
- package/skills/design-planning/ui-motion-interaction-standard/SKILL.md +69 -0
- package/skills/design-planning/ui-sdd-specialized/SKILL.md +5 -14
- package/skills/development-execution/flutter-errors/SKILL.md +5 -15
- package/skills/flutter-skills/.github/dependabot.yaml +15 -0
- package/skills/flutter-skills/.github/workflows/dart_skills_lint_workflow.yaml +68 -0
- package/skills/flutter-skills/.github/workflows/skills_tool.yaml +51 -0
- package/skills/flutter-skills/CODE_OF_CONDUCT.md +3 -0
- package/skills/flutter-skills/CONTRIBUTING.md +36 -0
- package/skills/flutter-skills/LICENSE +26 -0
- package/skills/flutter-skills/README.md +50 -0
- package/skills/flutter-skills/pubspec.yaml +9 -0
- package/skills/flutter-skills/resources/flutter_skills.yaml +434 -0
- package/skills/flutter-skills/skills/flutter-add-integration-test/SKILL.md +163 -0
- package/skills/flutter-skills/skills/flutter-add-widget-preview/SKILL.md +145 -0
- package/skills/flutter-skills/skills/flutter-add-widget-test/SKILL.md +154 -0
- package/skills/flutter-skills/skills/flutter-apply-architecture-best-practices/SKILL.md +162 -0
- package/skills/flutter-skills/skills/flutter-build-responsive-layout/SKILL.md +139 -0
- package/skills/flutter-skills/skills/flutter-fix-layout-issues/SKILL.md +130 -0
- package/skills/flutter-skills/skills/flutter-implement-json-serialization/SKILL.md +153 -0
- package/skills/flutter-skills/skills/flutter-setup-declarative-routing/SKILL.md +255 -0
- package/skills/flutter-skills/skills/flutter-setup-localization/SKILL.md +210 -0
- package/skills/flutter-skills/skills/flutter-use-http-package/SKILL.md +175 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/add-dart-lint-validation-rule/SKILL.md +196 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-best-practices/SKILL.md +65 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-checks-migration/SKILL.md +158 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-cli-app-best-practices/SKILL.md +168 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-doc-validation/SKILL.md +87 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-long-lines/SKILL.md +101 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-matcher-best-practices/SKILL.md +136 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-modern-features/SKILL.md +266 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-package-maintenance/SKILL.md +92 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/SKILL.md +92 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/lib/src/calculator.dart +7 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/pubspec.yaml +8 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/example/test/calculator_test.dart +11 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/interpret_coverage.dart +95 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/pubspec.yaml +6 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-coverage/scripts/test/interpret_coverage_test.dart +93 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/dart-test-fundamentals/SKILL.md +173 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/definition-of-done/SKILL.md +27 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/flutter_skills_ignore.json +3 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/grill-me/SKILL.md +10 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/ignore.json +3 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/test-driven-development/SKILL.md +371 -0
- package/skills/flutter-skills/tool/dart_skills_lint/.agents/skills/test-driven-development/testing-anti-patterns.md +299 -0
- package/skills/flutter-skills/tool/dart_skills_lint/AUTHORS +7 -0
- package/skills/flutter-skills/tool/dart_skills_lint/CHANGELOG.md +12 -0
- package/skills/flutter-skills/tool/dart_skills_lint/CONTRIBUTING.md +51 -0
- package/skills/flutter-skills/tool/dart_skills_lint/LICENSE +27 -0
- package/skills/flutter-skills/tool/dart_skills_lint/README.md +203 -0
- package/skills/flutter-skills/tool/dart_skills_lint/analysis_options.yaml +296 -0
- package/skills/flutter-skills/tool/dart_skills_lint/bench/README.md +23 -0
- package/skills/flutter-skills/tool/dart_skills_lint/bench/baseline_throughput.dart +230 -0
- package/skills/flutter-skills/tool/dart_skills_lint/bin/cli.dart +10 -0
- package/skills/flutter-skills/tool/dart_skills_lint/dart_skills_lint.yaml +14 -0
- package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/PRODUCTION_READYNESS.md +48 -0
- package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/completion_migration_plan.md +99 -0
- package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/legacy_patterns_report.md +110 -0
- package/skills/flutter-skills/tool/dart_skills_lint/documentation/feature_design_docs/pub_vs_skill_report.md +56 -0
- package/skills/flutter-skills/tool/dart_skills_lint/documentation/knowledge/SPECIFICATION.md +79 -0
- package/skills/flutter-skills/tool/dart_skills_lint/documentation/knowledge/architecture_overview.md +64 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/dart_skills_lint.dart +11 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/config_parser.dart +156 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/entry_point.dart +354 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/fixable_rule.dart +20 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/analysis_severity.dart +15 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/check_type.dart +17 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/ignore_entry.dart +34 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/ignore_entry.g.dart +19 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skill_context.dart +27 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skill_rule.dart +27 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skills_ignores.dart +26 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/skills_ignores.g.dart +24 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/models/validation_error.dart +31 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rule_registry.dart +79 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/absolute_paths_rule.dart +74 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/description_length_rule.dart +49 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/disallowed_field_rule.dart +61 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/name_format_rule.dart +167 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/relative_paths_rule.dart +72 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/trailing_whitespace_rule.dart +93 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/rules/valid_yaml_metadata_rule.dart +74 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/skills_ignores_storage.dart +36 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/validation_session.dart +559 -0
- package/skills/flutter-skills/tool/dart_skills_lint/lib/src/validator.dart +238 -0
- package/skills/flutter-skills/tool/dart_skills_lint/pubspec.yaml +28 -0
- package/skills/flutter-skills/tool/dart_skills_lint/skills/README.md +10 -0
- package/skills/flutter-skills/tool/dart_skills_lint/skills/dart-skills-lint-validation/SKILL.md +195 -0
- package/skills/flutter-skills/tool/dart_skills_lint/skills-lock.json +75 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/absolute_paths_test.dart +167 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/cli_integration_test.dart +683 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/config_file_test.dart +292 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/custom_rule_test.dart +122 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/directory_structure_test.dart +163 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/field_constraints_test.dart +178 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/fixer_test.dart +172 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/ignore_models_test.dart +63 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/metadata_validation_test.dart +116 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/relative_path_flag_test.dart +70 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/relative_paths_test.dart +172 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/resolve_rules_test.dart +82 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/rule_naming_test.dart +29 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/skills_ignores_storage_test.dart +89 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/test_utils.dart +19 -0
- package/skills/flutter-skills/tool/dart_skills_lint/test/trailing_whitespace_test.dart +152 -0
- package/skills/flutter-skills/tool/generator/README.md +150 -0
- package/skills/flutter-skills/tool/generator/analysis_options.yaml +143 -0
- package/skills/flutter-skills/tool/generator/bin/skills.dart +73 -0
- package/skills/flutter-skills/tool/generator/lib/src/commands/base_skill_command.dart +87 -0
- package/skills/flutter-skills/tool/generator/lib/src/commands/base_yaml_command.dart +83 -0
- package/skills/flutter-skills/tool/generator/lib/src/commands/generate_skill_command.dart +92 -0
- package/skills/flutter-skills/tool/generator/lib/src/commands/update_readme_command.dart +150 -0
- package/skills/flutter-skills/tool/generator/lib/src/commands/update_skill_command.dart +97 -0
- package/skills/flutter-skills/tool/generator/lib/src/commands/validate_skill_command.dart +284 -0
- package/skills/flutter-skills/tool/generator/lib/src/models/skill_params.dart +41 -0
- package/skills/flutter-skills/tool/generator/lib/src/services/gemini_service.dart +310 -0
- package/skills/flutter-skills/tool/generator/lib/src/services/markdown_converter.dart +226 -0
- package/skills/flutter-skills/tool/generator/lib/src/services/prompts.dart +72 -0
- package/skills/flutter-skills/tool/generator/lib/src/services/resource_fetcher_service.dart +84 -0
- package/skills/flutter-skills/tool/generator/lib/src/services/skill_instructions.dart +30 -0
- package/skills/flutter-skills/tool/generator/pubspec.yaml +32 -0
- package/skills/flutter-skills/tool/generator/test/commands/base_skill_command_test.dart +131 -0
- package/skills/flutter-skills/tool/generator/test/commands/validate_skills_input_test.dart +263 -0
- package/skills/flutter-skills/tool/generator/test/custom_skill_rules/last_modified_rule.dart +32 -0
- package/skills/flutter-skills/tool/generator/test/generate_skills_retry_test.dart +105 -0
- package/skills/flutter-skills/tool/generator/test/generate_skills_test.dart +519 -0
- package/skills/flutter-skills/tool/generator/test/lint_skills_test.dart +34 -0
- package/skills/flutter-skills/tool/generator/test/markdown_converter_test.dart +103 -0
- package/skills/flutter-skills/tool/generator/test/markdown_table_test.dart +131 -0
- package/skills/flutter-skills/tool/generator/test/models/skill_params_test.dart +37 -0
- package/skills/flutter-skills/tool/generator/test/services/gemini_service_test.dart +291 -0
- package/skills/flutter-skills/tool/generator/test/services/markdown_converter_test.dart +156 -0
- package/skills/flutter-skills/tool/generator/test/services/resource_fetcher_service_test.dart +188 -0
- package/skills/flutter-skills/tool/generator/test/update_skills_test.dart +241 -0
- package/skills/flutter-skills/tool/generator/test/validate_skills_test.dart +728 -0
- package/skills/quality-assurance/bdd-acceptance/SKILL.md +5 -14
- package/skills/quality-assurance/flutter-test/SKILL.md +5 -16
- package/skills/rules/project_rules.md +538 -127
- package/skills/special-tools/env-check/SKILL.md +5 -13
- package/skills/special-tools/ios-full-auto-debug/SKILL.md +5 -15
- package/skills/writing-skills/SKILL.md +654 -0
- package/skills/writing-skills/anthropic-best-practices.md +1149 -0
- package/skills/writing-skills/examples/CLAUDE_MD_TESTING.md +189 -0
- package/skills/writing-skills/graphviz-conventions.dot +172 -0
- package/skills/writing-skills/persuasion-principles.md +187 -0
- package/skills/writing-skills/render-graphs.js +168 -0
- package/skills/writing-skills/testing-skills-with-subagents.md +384 -0
- package/skills/checklist.md +0 -154
- package/skills/rules/user_rules.md +0 -263
- 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
- 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
- package/skills//346/212/200/350/203/275/344/275/277/347/224/250/346/214/207/345/215/227.md +0 -309
- 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 '';
|
|
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 '[]($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
|
+
}
|