@gitpagedocs/cli 1.1.61 → 1.1.63

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 (72) hide show
  1. package/README.md +193 -191
  2. package/ai/application/ai-command.ts +24 -24
  3. package/ai/application/ai-provider-factory.ts +22 -22
  4. package/ai/application/docs-pattern.ts +68 -68
  5. package/ai/application/run-ai-cli-command.ts +167 -167
  6. package/ai/config.ts +50 -50
  7. package/ai/core/models/ai-cli-config.ts +33 -33
  8. package/ai/core/models/parse-ai-cli-config.ts +78 -78
  9. package/ai/core/ports/illm-provider.ts +32 -32
  10. package/ai/infrastructure/ai-config-file.ts +101 -101
  11. package/ai/infrastructure/file-system-adapter.ts +85 -85
  12. package/ai/infrastructure/llm/tools-llm-provider.ts +54 -54
  13. package/ai/infrastructure/version-docs-writer.ts +143 -143
  14. package/ai/presentation/ai-prompts.ts +239 -239
  15. package/application/config-only/handler.mjs +2 -0
  16. package/application/home/handler.mjs +2 -0
  17. package/application/ports/cli-runtime-ports.ts +91 -87
  18. package/application/report/config-only-reporter.mjs +64 -63
  19. package/application/use-cases/dispatch-mode.ts +18 -18
  20. package/builders/config-orchestrator.mjs +34 -34
  21. package/builders/project-links.mjs +50 -26
  22. package/builders/root-config-builder.d.mts +16 -0
  23. package/builders/root-config-builder.mjs +61 -53
  24. package/builders/route-builders.mjs +136 -136
  25. package/builders/theme-template.mjs +76 -76
  26. package/builders/version-config-builder.mjs +186 -186
  27. package/content/docs-en.mjs +549 -545
  28. package/content/docs-es.mjs +462 -460
  29. package/content/docs-pt.mjs +462 -460
  30. package/contracts/doc-versions.mjs +3 -3
  31. package/contracts/index.mjs +10 -0
  32. package/contracts/layouts-paths.d.mts +17 -0
  33. package/contracts/layouts-paths.mjs +68 -0
  34. package/data/default-site-config.mjs +321 -321
  35. package/data/i18n-langmenu.mjs +347 -347
  36. package/data/layouts.mjs +436 -436
  37. package/data/path-mappings.mjs +26 -26
  38. package/data/route-metas.mjs +93 -93
  39. package/data/theme-colors.mjs +443 -443
  40. package/data/urls.mjs +12 -12
  41. package/domain/models/cli-options.ts +33 -18
  42. package/domain/services/layouts-migration.d.mts +15 -0
  43. package/domain/services/layouts-migration.mjs +60 -0
  44. package/home/constants.mjs +4 -4
  45. package/index.mjs +37 -37
  46. package/infrastructure/gitpagedocs-config-file.ts +47 -47
  47. package/infrastructure/home/runtime.mjs +37 -37
  48. package/package.json +3 -3
  49. package/presentation/commands/ai-info.ts +57 -57
  50. package/presentation/commands/config-clear.ts +23 -23
  51. package/presentation/commands/config-info.ts +24 -24
  52. package/presentation/commands/diagnostics.ts +164 -164
  53. package/presentation/commands/docs.ts +37 -37
  54. package/presentation/commands/mcp.ts +20 -20
  55. package/presentation/commands/pages.ts +101 -101
  56. package/presentation/commands/password.ts +62 -62
  57. package/presentation/commands/run-command.ts +70 -70
  58. package/presentation/index.ts +124 -116
  59. package/presentation/options/parser.ts +119 -104
  60. package/presentation/options/resolver.ts +46 -48
  61. package/presentation/options/schema.ts +4 -0
  62. package/presentation/ui/clack.ts +91 -91
  63. package/presentation/ui/layouts-prompts.ts +124 -0
  64. package/presentation/ui/prompts.ts +146 -138
  65. package/presentation/ui/tty.ts +37 -0
  66. package/runtime/git-ops.mjs +110 -110
  67. package/runtime/layouts-migrator.d.mts +11 -0
  68. package/runtime/layouts-migrator.mjs +103 -0
  69. package/runtime/output.mjs +159 -146
  70. package/runtime/workflow.mjs +129 -129
  71. package/start.mjs +12 -12
  72. package/ui/logger.mjs +9 -9
@@ -1,32 +1,32 @@
1
- /**
2
- * @file illm-provider.ts
3
- * @description Contract for all LLM implementations ensuring OCP and DIP.
4
- * Isolates the AI logic from the specific LLM providers (OpenAI, Claude, Ollama, etc.).
5
- */
6
-
7
- export interface ChatMessage {
8
- role: 'user' | 'assistant' | 'system';
9
- content: string;
10
- type?: 'text' | 'image' | 'audio'; // Supporting multimedia as requested
11
- }
12
-
13
- export interface ILLMProviderConfig {
14
- apiKey?: string;
15
- baseUrl?: string;
16
- model?: string;
17
- }
18
-
19
- export interface ILLMProvider {
20
- /**
21
- * Generates markdown documentation based on file content.
22
- * @param fileContent Raw content of the source file
23
- * @param contextPrompt Optional instructions to guide the generation (e.g., FSD standard)
24
- */
25
- generateDocumentation(fileContent: string, contextPrompt?: string): Promise<string>;
26
-
27
- /**
28
- * Conversational chat support.
29
- * @param messages Array of chat messages (system context, user history, etc.)
30
- */
31
- chat(messages: ChatMessage[]): Promise<string>;
32
- }
1
+ /**
2
+ * @file illm-provider.ts
3
+ * @description Contract for all LLM implementations ensuring OCP and DIP.
4
+ * Isolates the AI logic from the specific LLM providers (OpenAI, Claude, Ollama, etc.).
5
+ */
6
+
7
+ export interface ChatMessage {
8
+ role: 'user' | 'assistant' | 'system';
9
+ content: string;
10
+ type?: 'text' | 'image' | 'audio'; // Supporting multimedia as requested
11
+ }
12
+
13
+ export interface ILLMProviderConfig {
14
+ apiKey?: string;
15
+ baseUrl?: string;
16
+ model?: string;
17
+ }
18
+
19
+ export interface ILLMProvider {
20
+ /**
21
+ * Generates markdown documentation based on file content.
22
+ * @param fileContent Raw content of the source file
23
+ * @param contextPrompt Optional instructions to guide the generation (e.g., FSD standard)
24
+ */
25
+ generateDocumentation(fileContent: string, contextPrompt?: string): Promise<string>;
26
+
27
+ /**
28
+ * Conversational chat support.
29
+ * @param messages Array of chat messages (system context, user history, etc.)
30
+ */
31
+ chat(messages: ChatMessage[]): Promise<string>;
32
+ }
@@ -1,101 +1,101 @@
1
- import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { resolveUserConfigDir } from "@gitpagedocs/tools";
4
- import {
5
- AI_CLI_CONFIG_FILENAME,
6
- type AiCliConfig,
7
- } from "../core/models/ai-cli-config";
8
- import { parseAiCliConfig } from "../core/models/parse-ai-cli-config";
9
-
10
- /** Owner-only directory/file permissions (no-op on Windows, enforced on POSIX). */
11
- const CONFIG_DIR_MODE = 0o700;
12
- const CONFIG_FILE_MODE = 0o600;
13
-
14
- export interface AiConfigFileRepositoryOptions {
15
- /** Working directory searched for a legacy `<cwd>/.gitpagedocsconfig`. */
16
- cwd?: string;
17
- /** Destination directory; defaults to the per-user OS config directory. */
18
- configDir?: string;
19
- /** Notified after a legacy config is moved into the user config directory. */
20
- onMigrate?: (fromPath: string, toPath: string) => void;
21
- }
22
-
23
- /**
24
- * Stores `.gitpagedocsconfig` in the per-user OS config directory
25
- * (`%APPDATA%\gitpagedocs` on Windows, `~/Library/Application Support/gitpagedocs`
26
- * on macOS, `$XDG_CONFIG_HOME/gitpagedocs` elsewhere) with owner-only
27
- * permissions, so the API key never lives inside a repository checkout.
28
- * A legacy `<cwd>/.gitpagedocsconfig` is migrated automatically on first read.
29
- */
30
- export class AiConfigFileRepository {
31
- private readonly cwd: string;
32
- private readonly configDir: string;
33
- private readonly onMigrate?: (fromPath: string, toPath: string) => void;
34
-
35
- constructor(options: AiConfigFileRepositoryOptions = {}) {
36
- this.cwd = options.cwd ?? process.cwd();
37
- this.configDir = options.configDir ?? resolveUserConfigDir();
38
- this.onMigrate = options.onMigrate;
39
- }
40
-
41
- getConfigPath(): string {
42
- return path.join(this.configDir, AI_CLI_CONFIG_FILENAME);
43
- }
44
-
45
- getLegacyConfigPath(): string {
46
- return path.join(this.cwd, AI_CLI_CONFIG_FILENAME);
47
- }
48
-
49
- async read(): Promise<AiCliConfig | null> {
50
- const current = await this.readFrom(this.getConfigPath());
51
- if (current) return current;
52
- return this.migrateLegacyConfig();
53
- }
54
-
55
- /** Delete the stored configuration (secure and legacy locations), wiping
56
- * any saved credentials. Returns the paths that were actually removed. */
57
- async clear(): Promise<string[]> {
58
- const removed: string[] = [];
59
- const candidates = new Set([this.getConfigPath(), this.getLegacyConfigPath()].map((p) => path.resolve(p)));
60
- for (const configPath of candidates) {
61
- try {
62
- await fs.rm(configPath);
63
- removed.push(configPath);
64
- } catch {
65
- // Missing file: nothing to remove at this location.
66
- }
67
- }
68
- return removed;
69
- }
70
-
71
- async write(config: AiCliConfig): Promise<void> {
72
- const configPath = this.getConfigPath();
73
- await fs.mkdir(path.dirname(configPath), { recursive: true, mode: CONFIG_DIR_MODE });
74
- const data = `${JSON.stringify(config, null, 2)}\n`;
75
- await fs.writeFile(configPath, data, { encoding: "utf-8", mode: CONFIG_FILE_MODE });
76
- }
77
-
78
- private async readFrom(configPath: string): Promise<AiCliConfig | null> {
79
- try {
80
- const content = await fs.readFile(configPath, "utf-8");
81
- return parseAiCliConfig(JSON.parse(content));
82
- } catch {
83
- return null;
84
- }
85
- }
86
-
87
- private async migrateLegacyConfig(): Promise<AiCliConfig | null> {
88
- const legacyPath = this.getLegacyConfigPath();
89
- if (path.resolve(legacyPath) === path.resolve(this.getConfigPath())) return null;
90
- const legacy = await this.readFrom(legacyPath);
91
- if (!legacy) return null;
92
- try {
93
- await this.write(legacy);
94
- await fs.rm(legacyPath, { force: true });
95
- this.onMigrate?.(legacyPath, this.getConfigPath());
96
- } catch {
97
- // Migration is best-effort: keep serving the legacy config on failure.
98
- }
99
- return legacy;
100
- }
101
- }
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveUserConfigDir } from "@gitpagedocs/tools";
4
+ import {
5
+ AI_CLI_CONFIG_FILENAME,
6
+ type AiCliConfig,
7
+ } from "../core/models/ai-cli-config";
8
+ import { parseAiCliConfig } from "../core/models/parse-ai-cli-config";
9
+
10
+ /** Owner-only directory/file permissions (no-op on Windows, enforced on POSIX). */
11
+ const CONFIG_DIR_MODE = 0o700;
12
+ const CONFIG_FILE_MODE = 0o600;
13
+
14
+ export interface AiConfigFileRepositoryOptions {
15
+ /** Working directory searched for a legacy `<cwd>/.gitpagedocsconfig`. */
16
+ cwd?: string;
17
+ /** Destination directory; defaults to the per-user OS config directory. */
18
+ configDir?: string;
19
+ /** Notified after a legacy config is moved into the user config directory. */
20
+ onMigrate?: (fromPath: string, toPath: string) => void;
21
+ }
22
+
23
+ /**
24
+ * Stores `.gitpagedocsconfig` in the per-user OS config directory
25
+ * (`%APPDATA%\gitpagedocs` on Windows, `~/Library/Application Support/gitpagedocs`
26
+ * on macOS, `$XDG_CONFIG_HOME/gitpagedocs` elsewhere) with owner-only
27
+ * permissions, so the API key never lives inside a repository checkout.
28
+ * A legacy `<cwd>/.gitpagedocsconfig` is migrated automatically on first read.
29
+ */
30
+ export class AiConfigFileRepository {
31
+ private readonly cwd: string;
32
+ private readonly configDir: string;
33
+ private readonly onMigrate?: (fromPath: string, toPath: string) => void;
34
+
35
+ constructor(options: AiConfigFileRepositoryOptions = {}) {
36
+ this.cwd = options.cwd ?? process.cwd();
37
+ this.configDir = options.configDir ?? resolveUserConfigDir();
38
+ this.onMigrate = options.onMigrate;
39
+ }
40
+
41
+ getConfigPath(): string {
42
+ return path.join(this.configDir, AI_CLI_CONFIG_FILENAME);
43
+ }
44
+
45
+ getLegacyConfigPath(): string {
46
+ return path.join(this.cwd, AI_CLI_CONFIG_FILENAME);
47
+ }
48
+
49
+ async read(): Promise<AiCliConfig | null> {
50
+ const current = await this.readFrom(this.getConfigPath());
51
+ if (current) return current;
52
+ return this.migrateLegacyConfig();
53
+ }
54
+
55
+ /** Delete the stored configuration (secure and legacy locations), wiping
56
+ * any saved credentials. Returns the paths that were actually removed. */
57
+ async clear(): Promise<string[]> {
58
+ const removed: string[] = [];
59
+ const candidates = new Set([this.getConfigPath(), this.getLegacyConfigPath()].map((p) => path.resolve(p)));
60
+ for (const configPath of candidates) {
61
+ try {
62
+ await fs.rm(configPath);
63
+ removed.push(configPath);
64
+ } catch {
65
+ // Missing file: nothing to remove at this location.
66
+ }
67
+ }
68
+ return removed;
69
+ }
70
+
71
+ async write(config: AiCliConfig): Promise<void> {
72
+ const configPath = this.getConfigPath();
73
+ await fs.mkdir(path.dirname(configPath), { recursive: true, mode: CONFIG_DIR_MODE });
74
+ const data = `${JSON.stringify(config, null, 2)}\n`;
75
+ await fs.writeFile(configPath, data, { encoding: "utf-8", mode: CONFIG_FILE_MODE });
76
+ }
77
+
78
+ private async readFrom(configPath: string): Promise<AiCliConfig | null> {
79
+ try {
80
+ const content = await fs.readFile(configPath, "utf-8");
81
+ return parseAiCliConfig(JSON.parse(content));
82
+ } catch {
83
+ return null;
84
+ }
85
+ }
86
+
87
+ private async migrateLegacyConfig(): Promise<AiCliConfig | null> {
88
+ const legacyPath = this.getLegacyConfigPath();
89
+ if (path.resolve(legacyPath) === path.resolve(this.getConfigPath())) return null;
90
+ const legacy = await this.readFrom(legacyPath);
91
+ if (!legacy) return null;
92
+ try {
93
+ await this.write(legacy);
94
+ await fs.rm(legacyPath, { force: true });
95
+ this.onMigrate?.(legacyPath, this.getConfigPath());
96
+ } catch {
97
+ // Migration is best-effort: keep serving the legacy config on failure.
98
+ }
99
+ return legacy;
100
+ }
101
+ }
@@ -1,85 +1,85 @@
1
- import fs from 'node:fs/promises';
2
- import path from 'node:path';
3
- import { statSync } from 'node:fs';
4
-
5
- export interface FilePayload {
6
- filePath: string;
7
- content: string;
8
- }
9
-
10
- export class FileSystemAdapter {
11
- constructor(private readonly baseDir: string = process.cwd()) {}
12
-
13
- splitExistingDirectories(targetDirectories: string[]): { existing: string[]; missing: string[] } {
14
- const existing: string[] = [];
15
- const missing: string[] = [];
16
-
17
- for (const targetDirectory of targetDirectories) {
18
- const absolutePath = path.resolve(this.baseDir, targetDirectory);
19
- try {
20
- const stat = statSync(absolutePath);
21
- if (stat.isDirectory()) {
22
- existing.push(targetDirectory);
23
- } else {
24
- missing.push(targetDirectory);
25
- }
26
- } catch {
27
- missing.push(targetDirectory);
28
- }
29
- }
30
-
31
- return { existing, missing };
32
- }
33
-
34
- async readDirectoryRecursively(targetDirectory: string): Promise<FilePayload[]> {
35
- const absolutePath = path.resolve(this.baseDir, targetDirectory);
36
- const resolvedFiles: FilePayload[] = [];
37
-
38
- try {
39
- await this.traverse(absolutePath, resolvedFiles);
40
- } catch (error) {
41
- }
42
-
43
- return resolvedFiles;
44
- }
45
-
46
- private async traverse(currentPath: string, payloadList: FilePayload[]): Promise<void> {
47
- let stats;
48
- try {
49
- stats = await fs.stat(currentPath);
50
- } catch {
51
- return;
52
- }
53
-
54
- if (stats.isDirectory()) {
55
- const basename = path.basename(currentPath);
56
- if (['node_modules', '.git', '.next', 'out', 'dist'].includes(basename)) {
57
- return;
58
- }
59
-
60
- const entries = await fs.readdir(currentPath);
61
- for (const entry of entries) {
62
- await this.traverse(path.join(currentPath, entry), payloadList);
63
- }
64
- } else if (stats.isFile()) {
65
- const ext = path.extname(currentPath);
66
- const validExtensions = ['.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.css'];
67
- if (validExtensions.includes(ext)) {
68
- try {
69
- const content = await fs.readFile(currentPath, 'utf-8');
70
- payloadList.push({
71
- filePath: currentPath,
72
- content: content
73
- });
74
- } catch {
75
- }
76
- }
77
- }
78
- }
79
-
80
- async writeDocumentationOutput(outputContent: string, targetPath: string = "documentation-output.md"): Promise<void> {
81
- const absoluteDest = path.resolve(this.baseDir, targetPath);
82
- await fs.mkdir(path.dirname(absoluteDest), { recursive: true });
83
- await fs.writeFile(absoluteDest, outputContent, 'utf-8');
84
- }
85
- }
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { statSync } from 'node:fs';
4
+
5
+ export interface FilePayload {
6
+ filePath: string;
7
+ content: string;
8
+ }
9
+
10
+ export class FileSystemAdapter {
11
+ constructor(private readonly baseDir: string = process.cwd()) {}
12
+
13
+ splitExistingDirectories(targetDirectories: string[]): { existing: string[]; missing: string[] } {
14
+ const existing: string[] = [];
15
+ const missing: string[] = [];
16
+
17
+ for (const targetDirectory of targetDirectories) {
18
+ const absolutePath = path.resolve(this.baseDir, targetDirectory);
19
+ try {
20
+ const stat = statSync(absolutePath);
21
+ if (stat.isDirectory()) {
22
+ existing.push(targetDirectory);
23
+ } else {
24
+ missing.push(targetDirectory);
25
+ }
26
+ } catch {
27
+ missing.push(targetDirectory);
28
+ }
29
+ }
30
+
31
+ return { existing, missing };
32
+ }
33
+
34
+ async readDirectoryRecursively(targetDirectory: string): Promise<FilePayload[]> {
35
+ const absolutePath = path.resolve(this.baseDir, targetDirectory);
36
+ const resolvedFiles: FilePayload[] = [];
37
+
38
+ try {
39
+ await this.traverse(absolutePath, resolvedFiles);
40
+ } catch (error) {
41
+ }
42
+
43
+ return resolvedFiles;
44
+ }
45
+
46
+ private async traverse(currentPath: string, payloadList: FilePayload[]): Promise<void> {
47
+ let stats;
48
+ try {
49
+ stats = await fs.stat(currentPath);
50
+ } catch {
51
+ return;
52
+ }
53
+
54
+ if (stats.isDirectory()) {
55
+ const basename = path.basename(currentPath);
56
+ if (['node_modules', '.git', '.next', 'out', 'dist'].includes(basename)) {
57
+ return;
58
+ }
59
+
60
+ const entries = await fs.readdir(currentPath);
61
+ for (const entry of entries) {
62
+ await this.traverse(path.join(currentPath, entry), payloadList);
63
+ }
64
+ } else if (stats.isFile()) {
65
+ const ext = path.extname(currentPath);
66
+ const validExtensions = ['.ts', '.tsx', '.js', '.jsx', '.json', '.md', '.css'];
67
+ if (validExtensions.includes(ext)) {
68
+ try {
69
+ const content = await fs.readFile(currentPath, 'utf-8');
70
+ payloadList.push({
71
+ filePath: currentPath,
72
+ content: content
73
+ });
74
+ } catch {
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ async writeDocumentationOutput(outputContent: string, targetPath: string = "documentation-output.md"): Promise<void> {
81
+ const absoluteDest = path.resolve(this.baseDir, targetPath);
82
+ await fs.mkdir(path.dirname(absoluteDest), { recursive: true });
83
+ await fs.writeFile(absoluteDest, outputContent, 'utf-8');
84
+ }
85
+ }
@@ -1,54 +1,54 @@
1
- import { createDefaultFactory, legacyProviderToCatalogId } from "@gitpagedocs/tools/ai";
2
- import type { AiMessage, ProviderConfig } from "@gitpagedocs/tools/ports";
3
- import type { ILLMProvider, ChatMessage, ILLMProviderConfig } from "../../core/ports/illm-provider";
4
- import { DEFAULT_AI_DOC_PROMPT, type AiProviderId } from "../../config";
5
-
6
- const factory = createDefaultFactory();
7
-
8
- /**
9
- * Single CLI LLM provider backed by the shared @gitpagedocs/tools AI core.
10
- * Replaces the four duplicated CLI provider classes (OpenAI/Claude/Gemini/
11
- * Ollama) with one adapter over the registry/factory, preserving the
12
- * ILLMProvider contract used by the AI documentation flow.
13
- */
14
- export class ToolsLlmProvider implements ILLMProvider {
15
- private readonly providerId;
16
- private readonly model: string;
17
- private readonly apiKey?: string;
18
- private readonly baseUrl?: string;
19
-
20
- constructor(legacyProvider: AiProviderId, config: ILLMProviderConfig) {
21
- this.providerId = legacyProviderToCatalogId(legacyProvider);
22
- this.model = config.model ?? "";
23
- this.apiKey = config.apiKey;
24
- this.baseUrl = config.baseUrl;
25
- }
26
-
27
- async generateDocumentation(fileContent: string, contextPrompt?: string): Promise<string> {
28
- return this.chat([
29
- { role: "system", content: contextPrompt || DEFAULT_AI_DOC_PROMPT },
30
- { role: "user", content: fileContent },
31
- ]);
32
- }
33
-
34
- async chat(messages: ChatMessage[]): Promise<string> {
35
- const system = messages
36
- .filter((m) => m.role === "system")
37
- .map((m) => m.content)
38
- .join("\n");
39
- const aiMessages: AiMessage[] = messages
40
- .filter((m) => m.role !== "system")
41
- .map((m) => ({ role: m.role as AiMessage["role"], content: m.content }));
42
-
43
- const config: ProviderConfig = {
44
- providerId: this.providerId,
45
- model: this.model,
46
- apiKey: this.apiKey,
47
- baseUrl: this.baseUrl,
48
- };
49
- const response = await factory
50
- .create(this.providerId)
51
- .generate({ messages: aiMessages, system: system || undefined, maxTokens: 4000 }, config);
52
- return response.text;
53
- }
54
- }
1
+ import { createDefaultFactory, legacyProviderToCatalogId } from "@gitpagedocs/tools/ai";
2
+ import type { AiMessage, ProviderConfig } from "@gitpagedocs/tools/ports";
3
+ import type { ILLMProvider, ChatMessage, ILLMProviderConfig } from "../../core/ports/illm-provider";
4
+ import { DEFAULT_AI_DOC_PROMPT, type AiProviderId } from "../../config";
5
+
6
+ const factory = createDefaultFactory();
7
+
8
+ /**
9
+ * Single CLI LLM provider backed by the shared @gitpagedocs/tools AI core.
10
+ * Replaces the four duplicated CLI provider classes (OpenAI/Claude/Gemini/
11
+ * Ollama) with one adapter over the registry/factory, preserving the
12
+ * ILLMProvider contract used by the AI documentation flow.
13
+ */
14
+ export class ToolsLlmProvider implements ILLMProvider {
15
+ private readonly providerId;
16
+ private readonly model: string;
17
+ private readonly apiKey?: string;
18
+ private readonly baseUrl?: string;
19
+
20
+ constructor(legacyProvider: AiProviderId, config: ILLMProviderConfig) {
21
+ this.providerId = legacyProviderToCatalogId(legacyProvider);
22
+ this.model = config.model ?? "";
23
+ this.apiKey = config.apiKey;
24
+ this.baseUrl = config.baseUrl;
25
+ }
26
+
27
+ async generateDocumentation(fileContent: string, contextPrompt?: string): Promise<string> {
28
+ return this.chat([
29
+ { role: "system", content: contextPrompt || DEFAULT_AI_DOC_PROMPT },
30
+ { role: "user", content: fileContent },
31
+ ]);
32
+ }
33
+
34
+ async chat(messages: ChatMessage[]): Promise<string> {
35
+ const system = messages
36
+ .filter((m) => m.role === "system")
37
+ .map((m) => m.content)
38
+ .join("\n");
39
+ const aiMessages: AiMessage[] = messages
40
+ .filter((m) => m.role !== "system")
41
+ .map((m) => ({ role: m.role as AiMessage["role"], content: m.content }));
42
+
43
+ const config: ProviderConfig = {
44
+ providerId: this.providerId,
45
+ model: this.model,
46
+ apiKey: this.apiKey,
47
+ baseUrl: this.baseUrl,
48
+ };
49
+ const response = await factory
50
+ .create(this.providerId)
51
+ .generate({ messages: aiMessages, system: system || undefined, maxTokens: 4000 }, config);
52
+ return response.text;
53
+ }
54
+ }