@gitpagedocs/cli 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -7
- package/ai/application/ai-credentials.ts +100 -0
- package/ai/application/resolve-chat-credentials.ts +12 -1
- package/ai/application/run-ai-cli-command.ts +46 -6
- package/ai/application/vault-password.ts +66 -0
- package/ai/config.ts +2 -2
- package/ai/core/models/ai-cli-config.ts +3 -0
- package/ai/core/models/parse-ai-cli-config.ts +2 -0
- package/ai/infrastructure/ai-key-vault.ts +65 -0
- package/ai/presentation/vault-prompts.ts +35 -0
- package/content/docs-en.mjs +5 -3
- package/content/docs-es.mjs +16 -1
- package/content/docs-pt.mjs +16 -1
- package/contracts/doc-versions.mjs +1 -1
- package/data/default-site-config.mjs +1 -0
- package/data/i18n-langmenu.mjs +27 -12
- package/package.json +3 -3
- package/presentation/commands/chat.ts +14 -0
- package/presentation/commands/config-clear.ts +5 -2
package/README.md
CHANGED
|
@@ -24,12 +24,13 @@ npx @gitpagedocs/cli
|
|
|
24
24
|
| `gitpagedocs --home` | Standalone distribution (`gitpagedocshome/`: static site + .env + Dockerfile) |
|
|
25
25
|
| `gitpagedocs -i` / `--interactive` | Interactive prompts |
|
|
26
26
|
| `gitpagedocs ai` | Interactive AI documentation generator — writes pages in the gitpagedocs pattern (see below) |
|
|
27
|
+
| `gitpagedocs chat [question]` | Streaming AI chat in the terminal — REPL on a TTY, one-shot with a question or piped stdin; `--provider`, `--model`, `--system` (see below) |
|
|
27
28
|
| `gitpagedocs provider [id]` · `models [provider]` | List AI providers / catalog models |
|
|
28
29
|
| `gitpagedocs document[:repo\|:file\|:folder]` | Generate documentation with AI in the gitpagedocs pattern |
|
|
29
30
|
| `gitpagedocs password` | Set a documentation access password (writes the public key to `config.json`, prints the private key) |
|
|
30
31
|
| `gitpagedocs deploy` / `pages [actions\|deploy]` | Configure GitHub Pages via Actions + push |
|
|
31
32
|
| `gitpagedocs docs` | Refresh managed regions of README/CONTRIBUTING/SECURITY |
|
|
32
|
-
| `gitpagedocs config clear` | Delete the stored `.gitpagedocsconfig` (wipes saved AI credentials) |
|
|
33
|
+
| `gitpagedocs config clear` | Delete the stored `.gitpagedocsconfig` **and** the encrypted key vault `.gitpagedocsvault` (wipes saved AI credentials) |
|
|
33
34
|
| `gitpagedocs doctor` · `version` · `update` | Diagnostics / version / registry update check |
|
|
34
35
|
| `gitpagedocs mcp start` | Start the MCP server over stdio |
|
|
35
36
|
|
|
@@ -43,12 +44,17 @@ The AI-CLI implementation lives in `cli/ai/` (relocated from the frontend so the
|
|
|
43
44
|
cli/
|
|
44
45
|
|-- index.mjs # Node entry (bootstraps TS presentation via tsx)
|
|
45
46
|
|-- package.json # the published `gitpagedocs` package
|
|
46
|
-
|-- ai/ # AI
|
|
47
|
+
|-- ai/ # AI CLI: docs generator + terminal chat
|
|
48
|
+
| |-- application/ # run-ai-cli-command, resolve-chat-credentials, ai-credentials (seal/unseal), vault-password
|
|
49
|
+
| |-- infrastructure/ # ai-config-file (.gitpagedocsconfig), ai-key-vault (.gitpagedocsvault), file-system-adapter, version-docs-writer
|
|
50
|
+
| |-- core/ # chat-session, config models + parser
|
|
51
|
+
| `-- presentation/ # @clack prompts (ai-prompts, vault-prompts)
|
|
47
52
|
|-- contracts/ # Stable contracts for external tooling
|
|
48
53
|
| `-- doc-versions.mjs
|
|
49
54
|
|
|
|
50
55
|
|-- presentation/ # Interface/composition root
|
|
51
56
|
| |-- index.ts
|
|
57
|
+
| |-- commands/ # chat, config-clear, config-info, docs, pages, password, mcp, diagnostics, ai-info
|
|
52
58
|
| |-- options/
|
|
53
59
|
| `-- ui/
|
|
54
60
|
|
|
|
@@ -139,7 +145,8 @@ npx @gitpagedocs/cli ai
|
|
|
139
145
|
- asks API key or Ollama URL
|
|
140
146
|
- asks paths to scan (supports one or many paths, including other repositories)
|
|
141
147
|
- generates documentation in `pt`, `en`, `es` **in the gitpagedocs pattern**
|
|
142
|
-
- optionally persists config in `.gitpagedocsconfig
|
|
148
|
+
- optionally persists config in `.gitpagedocsconfig`, sealing the API key in the encrypted
|
|
149
|
+
vault `.gitpagedocsvault` (vault password created on first use, asked on every run)
|
|
143
150
|
|
|
144
151
|
### gitpagedocs pattern output
|
|
145
152
|
|
|
@@ -161,8 +168,8 @@ permissions on POSIX systems:
|
|
|
161
168
|
- macOS: `~/Library/Application Support/gitpagedocs/.gitpagedocsconfig`
|
|
162
169
|
- Linux: `$XDG_CONFIG_HOME/gitpagedocs/.gitpagedocsconfig` (or `~/.config/gitpagedocs/.gitpagedocsconfig`)
|
|
163
170
|
|
|
164
|
-
Set `GITPAGEDOCS_CONFIG_DIR` to override the directory; delete the stored file
|
|
165
|
-
and
|
|
171
|
+
Set `GITPAGEDOCS_CONFIG_DIR` to override the directory; delete the stored file,
|
|
172
|
+
the vault and their credentials with `gitpagedocs config clear`; a legacy
|
|
166
173
|
`.gitpagedocsconfig` in the repository root is migrated automatically on the next
|
|
167
174
|
run. You can create/edit this file manually and then run `npx @gitpagedocs/cli ai`:
|
|
168
175
|
|
|
@@ -172,7 +179,7 @@ run. You can create/edit this file manually and then run `npx @gitpagedocs/cli a
|
|
|
172
179
|
"ai": {
|
|
173
180
|
"provider": "openai",
|
|
174
181
|
"model": "gpt-4o-mini",
|
|
175
|
-
"
|
|
182
|
+
"apiKeyEncrypted": true,
|
|
176
183
|
"paths": ["src", "cli", "../another-repo/src"],
|
|
177
184
|
"languages": ["pt", "en", "es"],
|
|
178
185
|
"outputDir": "gitpagedocs/docs",
|
|
@@ -182,7 +189,38 @@ run. You can create/edit this file manually and then run `npx @gitpagedocs/cli a
|
|
|
182
189
|
}
|
|
183
190
|
```
|
|
184
191
|
|
|
185
|
-
|
|
192
|
+
**The API key is not in this file.** Saving a configuration seals the key into the
|
|
193
|
+
encrypted vault `.gitpagedocsvault` in the same directory (`cli/ai/infrastructure/ai-key-vault.ts`
|
|
194
|
+
over `EncryptedCredentialVault` + `FileVaultStorage` from `@gitpagedocs/tools`: AES-256-GCM, key
|
|
195
|
+
derived from your password with PBKDF2-HMAC-SHA-256, 210k iterations) and the config only keeps
|
|
196
|
+
`"apiKeyEncrypted": true`.
|
|
197
|
+
|
|
198
|
+
- The **vault password is created on first use** (typed twice, at least 4 characters) and
|
|
199
|
+
**asked on every run** of `gitpagedocs ai` / `gitpagedocs chat` that uses the stored key
|
|
200
|
+
(3 attempts, then the run aborts with `Vault password rejected`).
|
|
201
|
+
- `GITPAGEDOCS_VAULT_PASSWORD` supplies the password without a prompt (CI, pipes, no TTY);
|
|
202
|
+
on a first run it initializes the vault, otherwise it must match the existing one.
|
|
203
|
+
- A legacy plaintext `"apiKey"` still present in an old file is sealed into the vault and
|
|
204
|
+
removed from the file the next time the config is read.
|
|
205
|
+
- If you hand-write the file, leave the key out: enter it in the `gitpagedocs ai` prompt (it is
|
|
206
|
+
sealed on save). `gitpagedocs chat` can also read the provider's environment variable
|
|
207
|
+
(`gitpagedocs provider <id>` lists it) when no key is stored.
|
|
208
|
+
|
|
209
|
+
For Ollama, use `baseUrl` instead of an API key (no vault involved).
|
|
210
|
+
|
|
211
|
+
### `gitpagedocs chat`
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
gitpagedocs chat # REPL on a TTY: /clear · /help · /exit (or Ctrl+C twice)
|
|
215
|
+
gitpagedocs chat "what does cli/ai do?" # one-shot answer, then exit
|
|
216
|
+
git diff | gitpagedocs chat "review this diff" # piped stdin is appended to the question
|
|
217
|
+
gitpagedocs chat --provider gemini --model gemini-2.5-pro --system "Answer in Portuguese" "..."
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Credentials are resolved in this order: `--provider`/`--model` flags, the stored
|
|
221
|
+
`.gitpagedocsconfig` (its key is decrypted after the vault password prompt, or from
|
|
222
|
+
`GITPAGEDOCS_VAULT_PASSWORD`), then the provider's environment variable. Output streams to
|
|
223
|
+
stdout while status lines go to stderr, so the answer can be piped.
|
|
186
224
|
|
|
187
225
|
### Interactive fallback
|
|
188
226
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { SecurityError } from "@gitpagedocs/tools";
|
|
2
|
+
import type { AiCliConfig } from "../core/models/ai-cli-config";
|
|
3
|
+
import type { AiConfigFileRepository } from "../infrastructure/ai-config-file";
|
|
4
|
+
import type { AiKeyVault } from "../infrastructure/ai-key-vault";
|
|
5
|
+
import { resolveVaultPassword, type VaultPasswordPrompt } from "./vault-password";
|
|
6
|
+
|
|
7
|
+
export type KeyVault = Pick<AiKeyVault, "isInitialized" | "initialize" | "unlock" | "setKey" | "getKey" | "getVaultPath">;
|
|
8
|
+
|
|
9
|
+
/** A config carries a key to protect (plaintext to migrate) or one sealed in the vault. */
|
|
10
|
+
export function needsVaultPassword(config: AiCliConfig): boolean {
|
|
11
|
+
return config.ai.apiKeyEncrypted === true || Boolean(config.ai.apiKey?.trim());
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SealApiKeyInput {
|
|
15
|
+
readonly config: AiCliConfig;
|
|
16
|
+
readonly vault: Pick<KeyVault, "setKey">;
|
|
17
|
+
readonly password: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Moves the plaintext API key into the vault and returns the config to persist:
|
|
22
|
+
* no `apiKey`, `apiKeyEncrypted: true`. A config without a key is returned as is.
|
|
23
|
+
*/
|
|
24
|
+
export async function sealApiKey({ config, vault, password }: SealApiKeyInput): Promise<AiCliConfig> {
|
|
25
|
+
const apiKey = config.ai.apiKey?.trim();
|
|
26
|
+
if (!apiKey) return config;
|
|
27
|
+
await vault.setKey(password, config.ai.provider, apiKey);
|
|
28
|
+
const ai = { ...config.ai };
|
|
29
|
+
delete ai.apiKey;
|
|
30
|
+
return { ...config, ai: { ...ai, apiKeyEncrypted: true } };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UnsealApiKeyInput {
|
|
34
|
+
readonly config: AiCliConfig;
|
|
35
|
+
readonly vault: Pick<KeyVault, "getKey">;
|
|
36
|
+
readonly password: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The stored config with its key decrypted into memory (never written back). */
|
|
40
|
+
export async function unsealApiKey({ config, vault, password }: UnsealApiKeyInput): Promise<AiCliConfig> {
|
|
41
|
+
if (!config.ai.apiKeyEncrypted) return config;
|
|
42
|
+
const apiKey = await vault.getKey(password, config.ai.provider);
|
|
43
|
+
return apiKey ? { ...config, ai: { ...config.ai, apiKey } } : config;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface UnlockStoredConfigInput {
|
|
47
|
+
readonly config: AiCliConfig;
|
|
48
|
+
readonly vault: KeyVault;
|
|
49
|
+
readonly configRepo: Pick<AiConfigFileRepository, "write">;
|
|
50
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
51
|
+
readonly prompt?: VaultPasswordPrompt;
|
|
52
|
+
readonly onInfo?: (message: string) => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Turns the stored config into one a run can use. An encrypted key is decrypted
|
|
57
|
+
* into memory after the password check; a legacy plaintext key is sealed into
|
|
58
|
+
* the vault and stripped from `.gitpagedocsconfig` on the spot, so the file
|
|
59
|
+
* never keeps a key in clear again.
|
|
60
|
+
*/
|
|
61
|
+
export async function unlockStoredConfig(input: UnlockStoredConfigInput): Promise<AiCliConfig> {
|
|
62
|
+
const { config } = input;
|
|
63
|
+
if (!needsVaultPassword(config)) return config;
|
|
64
|
+
const password = await resolveVaultPassword({ vault: input.vault, env: input.env, prompt: input.prompt });
|
|
65
|
+
|
|
66
|
+
if (config.ai.apiKeyEncrypted && !config.ai.apiKey?.trim()) {
|
|
67
|
+
const unsealed = await unsealApiKey({ config, vault: input.vault, password });
|
|
68
|
+
if (!unsealed.ai.apiKey) {
|
|
69
|
+
throw new SecurityError(
|
|
70
|
+
`No API key is stored for "${config.ai.provider}". Run "gitpagedocs ai" and enter the key again, or "gitpagedocs config clear".`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
return unsealed;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const sealed = await sealApiKey({ config, vault: input.vault, password });
|
|
77
|
+
await input.configRepo.write(sealed);
|
|
78
|
+
input.onInfo?.(
|
|
79
|
+
`[gitpagedocs:ai] API key moved from .gitpagedocsconfig into the encrypted vault (${input.vault.getVaultPath()}).`,
|
|
80
|
+
);
|
|
81
|
+
return config;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type StoredKeyUnlocker = (stored: AiCliConfig) => Promise<string | undefined>;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Callback for credential resolvers: the decrypted key of the stored config, or
|
|
88
|
+
* undefined (with the reason reported) when the vault cannot be opened.
|
|
89
|
+
*/
|
|
90
|
+
export function createStoredKeyUnlocker(deps: Omit<UnlockStoredConfigInput, "config">): StoredKeyUnlocker {
|
|
91
|
+
return async (stored) => {
|
|
92
|
+
try {
|
|
93
|
+
const unlocked = await unlockStoredConfig({ ...deps, config: stored });
|
|
94
|
+
return unlocked.ai.apiKey;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
deps.onInfo?.(error instanceof Error ? error.message : String(error));
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { AiProviderId } from "@gitpagedocs/tools/ports";
|
|
|
3
3
|
import { PROVIDER_CATALOG } from "@gitpagedocs/tools";
|
|
4
4
|
import { AiConfigFileRepository } from "../infrastructure/ai-config-file";
|
|
5
5
|
import type { AiCliConfig } from "../core/models/ai-cli-config";
|
|
6
|
+
import { needsVaultPassword, type StoredKeyUnlocker } from "./ai-credentials";
|
|
6
7
|
|
|
7
8
|
export interface ResolvedChatCredentials {
|
|
8
9
|
readonly providerId: AiProviderId;
|
|
@@ -21,6 +22,12 @@ export interface ResolveChatCredentialsInput {
|
|
|
21
22
|
readonly configRepo?: Pick<AiConfigFileRepository, "read">;
|
|
22
23
|
/** Injectable for tests; defaults to process.env. */
|
|
23
24
|
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
25
|
+
/**
|
|
26
|
+
* Decrypts the key of a stored config (asking the vault password) and
|
|
27
|
+
* migrates a legacy plaintext key into the vault. Without it a plaintext
|
|
28
|
+
* key is used as is and an encrypted one is unreachable.
|
|
29
|
+
*/
|
|
30
|
+
readonly unlockStoredKey?: StoredKeyUnlocker;
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
/**
|
|
@@ -40,7 +47,11 @@ export async function resolveChatCredentials(
|
|
|
40
47
|
const spec = PROVIDER_CATALOG[providerId];
|
|
41
48
|
const model = input.modelOverride?.trim() || stored?.ai.model?.trim() || spec.defaultModel;
|
|
42
49
|
const baseUrl = stored?.ai.baseUrl?.trim() || undefined;
|
|
43
|
-
const
|
|
50
|
+
const storedKey =
|
|
51
|
+
stored && input.unlockStoredKey && needsVaultPassword(stored)
|
|
52
|
+
? await input.unlockStoredKey(stored)
|
|
53
|
+
: stored?.ai.apiKey?.trim();
|
|
54
|
+
const apiKey = storedKey?.trim() || resolveApiKeyFromEnv(providerId, env);
|
|
44
55
|
|
|
45
56
|
const keyless = spec.auth === "none";
|
|
46
57
|
if (!keyless && !apiKey) return null;
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { AiCommandService } from "./ai-command";
|
|
2
2
|
import { createAiProvider } from "./ai-provider-factory";
|
|
3
|
+
import { sealApiKey, unlockStoredConfig } from "./ai-credentials";
|
|
4
|
+
import { resolveVaultPassword, type VaultPasswordPrompt } from "./vault-password";
|
|
3
5
|
import { FileSystemAdapter, type FilePayload } from "../infrastructure/file-system-adapter";
|
|
4
6
|
import {
|
|
5
7
|
AiConfigFileRepository,
|
|
6
8
|
} from "../infrastructure/ai-config-file";
|
|
7
|
-
import
|
|
9
|
+
import { AiKeyVault } from "../infrastructure/ai-key-vault";
|
|
10
|
+
import type { AiCliConfig, AiCliRunPlan, AiCliRunSummary } from "../core/models/ai-cli-config";
|
|
8
11
|
import { promptMissingDirectories, runAiInteractivePrompt } from "../presentation/ai-prompts";
|
|
12
|
+
import { clackVaultPasswordPrompt } from "../presentation/vault-prompts";
|
|
9
13
|
import { GITPAGEDOCS_DOC_SYSTEM_PROMPT } from "../config";
|
|
10
14
|
import { parseAiPages, type AiDocPage } from "./docs-pattern";
|
|
11
15
|
import { writeVersionDocs } from "../infrastructure/version-docs-writer";
|
|
@@ -130,26 +134,62 @@ export async function executeAiRunPlan(
|
|
|
130
134
|
};
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
export
|
|
137
|
+
export interface RunAiCliCommandOptions {
|
|
134
138
|
cwd: string;
|
|
135
139
|
onInfo?: (message: string) => void;
|
|
136
140
|
/** Provided by the composition root to scaffold the base gitpagedocs/ tree
|
|
137
141
|
* (so the version config.json exists before AI pages are wired into it). */
|
|
138
142
|
onScaffold?: () => Promise<void>;
|
|
139
|
-
|
|
143
|
+
/** Injectable for tests; defaults to the per-user vault file. */
|
|
144
|
+
vault?: AiKeyVault;
|
|
145
|
+
/** Injectable for tests; defaults to the terminal prompts. */
|
|
146
|
+
passwordPrompt?: VaultPasswordPrompt;
|
|
147
|
+
/** Injectable for tests; defaults to process.env. */
|
|
148
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
interface SaveConfigDeps {
|
|
152
|
+
configRepo: AiConfigFileRepository;
|
|
153
|
+
vault: AiKeyVault;
|
|
154
|
+
prompt: VaultPasswordPrompt;
|
|
155
|
+
env?: Readonly<Record<string, string | undefined>>;
|
|
156
|
+
logInfo: (message: string) => void;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Persists the plan's config; an API key is sealed into the vault first, never written in clear. */
|
|
160
|
+
async function saveConfig(config: AiCliConfig, deps: SaveConfigDeps): Promise<void> {
|
|
161
|
+
if (!config.ai.apiKey?.trim()) {
|
|
162
|
+
await deps.configRepo.write(config);
|
|
163
|
+
deps.logInfo(`[gitpagedocs:ai] Configuration saved to ${deps.configRepo.getConfigPath()}`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const password = await resolveVaultPassword({ vault: deps.vault, env: deps.env, prompt: deps.prompt });
|
|
167
|
+
await deps.configRepo.write(await sealApiKey({ config, vault: deps.vault, password }));
|
|
168
|
+
deps.logInfo(
|
|
169
|
+
`[gitpagedocs:ai] Configuration saved to ${deps.configRepo.getConfigPath()} (API key encrypted in ${deps.vault.getVaultPath()})`,
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function runAiCliCommand(options: RunAiCliCommandOptions): Promise<{ summary: AiCliRunSummary; runConfigScaffold: boolean }> {
|
|
140
174
|
const logInfo = options.onInfo ?? (() => undefined);
|
|
141
175
|
const configRepo = new AiConfigFileRepository({
|
|
142
176
|
cwd: options.cwd,
|
|
143
177
|
onMigrate: (fromPath, toPath) =>
|
|
144
178
|
logInfo(`[gitpagedocs:ai] Moved configuration from ${fromPath} to ${toPath} (secure user config directory).`),
|
|
145
179
|
});
|
|
180
|
+
const vault = options.vault ?? new AiKeyVault();
|
|
181
|
+
const prompt = options.passwordPrompt ?? clackVaultPasswordPrompt;
|
|
146
182
|
|
|
147
183
|
const existingConfig = await configRepo.read();
|
|
148
184
|
const plan = await runAiInteractivePrompt(existingConfig);
|
|
185
|
+
let runConfig = plan.config;
|
|
149
186
|
|
|
150
187
|
if (plan.saveConfig) {
|
|
151
|
-
await configRepo.
|
|
152
|
-
|
|
188
|
+
await saveConfig(runConfig, { configRepo, vault, prompt, env: options.env, logInfo });
|
|
189
|
+
} else if (existingConfig && runConfig === existingConfig) {
|
|
190
|
+
// Reusing the stored config: decrypt its key for this run (the password is
|
|
191
|
+
// asked every time), or seal a legacy plaintext key into the vault.
|
|
192
|
+
runConfig = await unlockStoredConfig({ config: runConfig, vault, configRepo, env: options.env, prompt, onInfo: logInfo });
|
|
153
193
|
}
|
|
154
194
|
|
|
155
195
|
// Build the base gitpagedocs structure BEFORE generation so the version
|
|
@@ -159,7 +199,7 @@ export async function runAiCliCommand(options: {
|
|
|
159
199
|
await options.onScaffold();
|
|
160
200
|
}
|
|
161
201
|
|
|
162
|
-
const summary = await executeAiRunPlan(plan, options.cwd);
|
|
202
|
+
const summary = await executeAiRunPlan({ ...plan, config: runConfig }, options.cwd);
|
|
163
203
|
return {
|
|
164
204
|
summary,
|
|
165
205
|
runConfigScaffold: plan.runConfigScaffold,
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { SecurityError } from "@gitpagedocs/tools";
|
|
2
|
+
import type { AiKeyVault } from "../infrastructure/ai-key-vault";
|
|
3
|
+
|
|
4
|
+
/** Non-interactive runs (CI, pipes) supply the vault password through this variable. */
|
|
5
|
+
export const VAULT_PASSWORD_ENV = "GITPAGEDOCS_VAULT_PASSWORD";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_MAX_ATTEMPTS = 3;
|
|
8
|
+
|
|
9
|
+
/** How the host asks for the password (terminal prompt, test double, ...). */
|
|
10
|
+
export interface VaultPasswordPrompt {
|
|
11
|
+
/** First run: let the user choose (and confirm) the password. */
|
|
12
|
+
create(): Promise<string>;
|
|
13
|
+
/** Later runs: ask for the existing password; `attempt` starts at 1. */
|
|
14
|
+
unlock(attempt: number): Promise<string>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type PasswordVault = Pick<AiKeyVault, "isInitialized" | "initialize" | "unlock">;
|
|
18
|
+
|
|
19
|
+
export interface ResolveVaultPasswordInput {
|
|
20
|
+
readonly vault: PasswordVault;
|
|
21
|
+
/** Defaults to process.env. */
|
|
22
|
+
readonly env?: Readonly<Record<string, string | undefined>>;
|
|
23
|
+
/** Absent when no terminal is available. */
|
|
24
|
+
readonly prompt?: VaultPasswordPrompt;
|
|
25
|
+
readonly maxAttempts?: number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The verified vault password for this run. Order: the environment variable
|
|
30
|
+
* (initializing the vault on first use), otherwise the prompt — create on first
|
|
31
|
+
* run, unlock afterwards with up to three attempts. The password is asked on
|
|
32
|
+
* every command run; it is never persisted.
|
|
33
|
+
*/
|
|
34
|
+
export async function resolveVaultPassword(input: ResolveVaultPasswordInput): Promise<string> {
|
|
35
|
+
const env = input.env ?? process.env;
|
|
36
|
+
const initialized = await input.vault.isInitialized();
|
|
37
|
+
|
|
38
|
+
const fromEnv = env[VAULT_PASSWORD_ENV]?.trim();
|
|
39
|
+
if (fromEnv) {
|
|
40
|
+
if (!initialized) {
|
|
41
|
+
await input.vault.initialize(fromEnv);
|
|
42
|
+
return fromEnv;
|
|
43
|
+
}
|
|
44
|
+
if (await input.vault.unlock(fromEnv)) return fromEnv;
|
|
45
|
+
throw new SecurityError(`${VAULT_PASSWORD_ENV} does not match the vault password.`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (!input.prompt) {
|
|
49
|
+
throw new SecurityError(
|
|
50
|
+
`A password is required to read the encrypted API key. Run the command in a terminal or set ${VAULT_PASSWORD_ENV}.`,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!initialized) {
|
|
55
|
+
const password = await input.prompt.create();
|
|
56
|
+
await input.vault.initialize(password);
|
|
57
|
+
return password;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const maxAttempts = input.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
61
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
62
|
+
const password = await input.prompt.unlock(attempt);
|
|
63
|
+
if (await input.vault.unlock(password)) return password;
|
|
64
|
+
}
|
|
65
|
+
throw new SecurityError(`Vault password rejected after ${maxAttempts} attempts.`);
|
|
66
|
+
}
|
package/ai/config.ts
CHANGED
|
@@ -11,8 +11,8 @@ export type AiProviderId = 'openai' | 'claude' | 'gemini' | 'ollama';
|
|
|
11
11
|
|
|
12
12
|
export const AI_MODEL_DEFAULTS: Readonly<Record<AiProviderId, string>> = {
|
|
13
13
|
openai: 'gpt-4o-mini',
|
|
14
|
-
claude: 'claude-
|
|
15
|
-
gemini: 'gemini-
|
|
14
|
+
claude: 'claude-sonnet-4-6',
|
|
15
|
+
gemini: 'gemini-2.5-flash',
|
|
16
16
|
ollama: 'llama3',
|
|
17
17
|
};
|
|
18
18
|
|
|
@@ -7,7 +7,10 @@ export interface AiCliConfig {
|
|
|
7
7
|
ai: {
|
|
8
8
|
provider: AiProviderId;
|
|
9
9
|
model: string;
|
|
10
|
+
/** Only in memory during a run: the file never stores the key in clear. */
|
|
10
11
|
apiKey?: string;
|
|
12
|
+
/** The key lives sealed in the `.gitpagedocsvault` next to this file. */
|
|
13
|
+
apiKeyEncrypted?: boolean;
|
|
11
14
|
baseUrl?: string;
|
|
12
15
|
paths: string[];
|
|
13
16
|
languages: Array<"pt" | "en" | "es">;
|
|
@@ -57,6 +57,7 @@ export function parseAiCliConfig(raw: unknown): AiCliConfig | null {
|
|
|
57
57
|
|
|
58
58
|
const model = asOptionalString(ai.model)?.trim() || AI_MODEL_DEFAULTS[provider];
|
|
59
59
|
const apiKey = asOptionalString(ai.apiKey);
|
|
60
|
+
const apiKeyEncrypted = ai.apiKeyEncrypted === true;
|
|
60
61
|
const baseUrl = asOptionalString(ai.baseUrl);
|
|
61
62
|
|
|
62
63
|
return {
|
|
@@ -65,6 +66,7 @@ export function parseAiCliConfig(raw: unknown): AiCliConfig | null {
|
|
|
65
66
|
provider,
|
|
66
67
|
model,
|
|
67
68
|
...(apiKey !== undefined ? { apiKey } : {}),
|
|
69
|
+
...(apiKeyEncrypted ? { apiKeyEncrypted: true } : {}),
|
|
68
70
|
...(baseUrl !== undefined ? { baseUrl } : {}),
|
|
69
71
|
paths: asPaths(ai.paths),
|
|
70
72
|
languages: asLanguages(ai.languages),
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import {
|
|
4
|
+
EncryptedCredentialVault,
|
|
5
|
+
FileVaultStorage,
|
|
6
|
+
NodeCryptoService,
|
|
7
|
+
resolveUserConfigDir,
|
|
8
|
+
} from "@gitpagedocs/tools";
|
|
9
|
+
|
|
10
|
+
export const AI_KEY_VAULT_FILENAME = ".gitpagedocsvault";
|
|
11
|
+
|
|
12
|
+
export interface AiKeyVaultOptions {
|
|
13
|
+
/** Directory of the vault file; defaults to the per-user OS config directory. */
|
|
14
|
+
configDir?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Password-encrypted store for the CLI's AI API keys, kept next to
|
|
19
|
+
* `.gitpagedocsconfig` in the per-user config directory. Every key is sealed
|
|
20
|
+
* with AES-256-GCM under a key derived from the password by PBKDF2-HMAC-SHA-256
|
|
21
|
+
* (210,000 iterations, random salt); the file never holds a key in clear and
|
|
22
|
+
* only the password decrypts it.
|
|
23
|
+
*/
|
|
24
|
+
export class AiKeyVault {
|
|
25
|
+
private readonly filePath: string;
|
|
26
|
+
private readonly vault: EncryptedCredentialVault;
|
|
27
|
+
|
|
28
|
+
constructor(options: AiKeyVaultOptions = {}) {
|
|
29
|
+
this.filePath = path.join(options.configDir ?? resolveUserConfigDir(), AI_KEY_VAULT_FILENAME);
|
|
30
|
+
this.vault = new EncryptedCredentialVault(new FileVaultStorage(this.filePath), new NodeCryptoService());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
getVaultPath(): string {
|
|
34
|
+
return this.filePath;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** True once a password has been set (the vault file exists). */
|
|
38
|
+
isInitialized(): Promise<boolean> {
|
|
39
|
+
return this.vault.isInitialized();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
initialize(password: string): Promise<void> {
|
|
43
|
+
return this.vault.initialize(password);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether `password` opens the vault; never throws for a wrong password. */
|
|
47
|
+
unlock(password: string): Promise<boolean> {
|
|
48
|
+
return this.vault.unlock(password);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setKey(password: string, providerId: string, apiKey: string): Promise<void> {
|
|
52
|
+
return this.vault.setCredential(password, providerId, apiKey);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
getKey(password: string, providerId: string): Promise<string | undefined> {
|
|
56
|
+
return this.vault.getCredential(password, providerId);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Deletes the vault file with every sealed key. Returns whether a file existed. */
|
|
60
|
+
async clear(): Promise<boolean> {
|
|
61
|
+
const existed = existsSync(this.filePath);
|
|
62
|
+
await this.vault.reset();
|
|
63
|
+
return existed;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { SecurityError } from "@gitpagedocs/tools";
|
|
2
|
+
import { askPassword, note } from "../../presentation/ui/clack";
|
|
3
|
+
import { VAULT_PASSWORD_ENV, type VaultPasswordPrompt } from "../application/vault-password";
|
|
4
|
+
|
|
5
|
+
const MIN_PASSWORD_LENGTH = 4;
|
|
6
|
+
|
|
7
|
+
function requireTerminal(): void {
|
|
8
|
+
if (!process.stdin.isTTY) {
|
|
9
|
+
throw new SecurityError(
|
|
10
|
+
`The encrypted API key needs its password. Run the command in a terminal or set ${VAULT_PASSWORD_ENV}.`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Terminal prompts for the vault password: create + confirm on first run, unlock afterwards. */
|
|
16
|
+
export const clackVaultPasswordPrompt: VaultPasswordPrompt = {
|
|
17
|
+
async create() {
|
|
18
|
+
requireTerminal();
|
|
19
|
+
for (;;) {
|
|
20
|
+
const password = await askPassword({
|
|
21
|
+
message: "Create a local password to encrypt your API key (asked on every run):",
|
|
22
|
+
validate: (value) => (value.trim().length >= MIN_PASSWORD_LENGTH ? undefined : `Use at least ${MIN_PASSWORD_LENGTH} characters.`),
|
|
23
|
+
});
|
|
24
|
+
const confirmation = await askPassword({ message: "Confirm the password:" });
|
|
25
|
+
if (password === confirmation) return password;
|
|
26
|
+
note("The passwords do not match. Try again.");
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
async unlock(attempt) {
|
|
30
|
+
requireTerminal();
|
|
31
|
+
return askPassword({
|
|
32
|
+
message: attempt > 1 ? `Local password (attempt ${attempt} of 3):` : "Local password to decrypt the API key:",
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
};
|
package/content/docs-en.mjs
CHANGED
|
@@ -187,9 +187,11 @@ The version config can render a **Source code** container through \`routes-sourc
|
|
|
187
187
|
The docs ship an AI assistant in two surfaces: an in-docs **chat drawer** (the AI chat button in the sidebar, enabled via \`site.AiChatEnabled\`) and a dedicated **\`/ai\` console** page.
|
|
188
188
|
|
|
189
189
|
- **14 providers** via one shared core: OpenAI, Anthropic, Gemini, OpenRouter, Ollama, Azure OpenAI, Mistral, DeepSeek, Cohere, Groq, xAI, Together, Fireworks, Perplexity.
|
|
190
|
-
- **Model selection** — pick from each provider's catalog (\`gitpagedocs models <provider>\`)
|
|
191
|
-
- **Encrypted at rest** — your API key is sealed with AES-256-GCM behind a **local password**
|
|
192
|
-
- **
|
|
190
|
+
- **Model selection** — pick from each provider's catalog (\`gitpagedocs models <provider>\`); a stored model id the provider has retired is replaced by the provider default automatically.
|
|
191
|
+
- **Encrypted at rest** — your API key is sealed with AES-256-GCM behind a **local password** and is never stored in plaintext or logged. A legacy plaintext key is migrated and wiped on first unlock.
|
|
192
|
+
- **Inactivity auto-lock** — the chat drawer locks itself after \`site.AiChatAutoLockSeconds\` idle seconds (default 30, \`0\` disables): a centered countdown modal in your language lets you cancel or lock now, and unlocking asks for the password again.
|
|
193
|
+
- **Resilient providers** — transient provider errors are retried (3 attempts) and a final failure is a plain message ending with "Try again!".
|
|
194
|
+
- **AI documentation generation** — \`gitpagedocs ai\` scans chosen paths and writes multilingual markdown (pt/en/es); reusable via \`.gitpagedocsconfig\`, whose API key lives sealed in the encrypted \`.gitpagedocsvault\` (the vault password is asked on every run). \`gitpagedocs chat\` brings the same assistant to the terminal.
|
|
193
195
|
|
|
194
196
|
## MCP server
|
|
195
197
|
|
package/content/docs-es.mjs
CHANGED
|
@@ -165,6 +165,21 @@ El config de version puede renderizar un contenedor **Codigo fuente** via \`rout
|
|
|
165
165
|
|
|
166
166
|
- \`GITPAGEDOCS_REPOSITORY_SEARCH\` – busqueda de repositorio (local)
|
|
167
167
|
- \`GITHUB_ACTIONS\` – modo build GitHub Pages
|
|
168
|
+
|
|
169
|
+
## Asistente de IA
|
|
170
|
+
|
|
171
|
+
Los docs traen un asistente de IA en dos superficies: un **panel de chat** dentro de los docs (boton de chat de IA en la barra lateral, activado por \`site.AiChatEnabled\`) y una pagina **consola \`/ai\`** dedicada.
|
|
172
|
+
|
|
173
|
+
- **14 proveedores** en un unico core compartido: OpenAI, Anthropic, Gemini, OpenRouter, Ollama, Azure OpenAI, Mistral, DeepSeek, Cohere, Groq, xAI, Together, Fireworks, Perplexity.
|
|
174
|
+
- **Eleccion de modelo** — desde el catalogo de cada proveedor (\`gitpagedocs models <proveedor>\`); un id de modelo guardado que el proveedor retiro se reemplaza por el predeterminado del proveedor automaticamente.
|
|
175
|
+
- **Cifrado en reposo** — tu clave de API se sella con AES-256-GCM detras de una **contrasena local** y nunca queda en texto plano ni en logs. Una clave heredada en texto plano se migra y se borra en el primer desbloqueo.
|
|
176
|
+
- **Bloqueo por inactividad** — el panel de chat se bloquea solo tras \`site.AiChatAutoLockSeconds\` segundos sin uso (por defecto 30, \`0\` lo desactiva): un modal centrado con cuenta regresiva, en tu idioma, permite cancelar o bloquear ahora, y desbloquear pide la contrasena de nuevo.
|
|
177
|
+
- **Proveedores resilientes** — los errores transitorios del proveedor se reintentan (3 intentos) y un fallo final es un mensaje simple que termina en "¡Inténtalo de nuevo!".
|
|
178
|
+
- **Generacion de documentacion con IA** — \`gitpagedocs ai\` recorre las rutas elegidas y escribe markdown multilingue (pt/en/es); reutilizable via \`.gitpagedocsconfig\`, cuya clave de API queda sellada en la boveda cifrada \`.gitpagedocsvault\` (la contrasena de la boveda se pide en cada ejecucion). \`gitpagedocs chat\` lleva el mismo asistente a la terminal.
|
|
179
|
+
|
|
180
|
+
## Servidor MCP
|
|
181
|
+
|
|
182
|
+
\`gitpagedocs mcp start\` levanta un servidor Model Context Protocol (stdio) que expone **20 tools** (sistema de archivos, IA, generacion/analisis de docs) y **7 resources** (\`project://structure|docs|config|repository|readme|ai/providers|ai/models\`) para editores y agentes de IA.
|
|
168
183
|
`,
|
|
169
184
|
configuration: `# Configuracion
|
|
170
185
|
|
|
@@ -311,7 +326,7 @@ El proyecto esta organizado por fronteras de feature y responsabilidades de runt
|
|
|
311
326
|
|
|
312
327
|
## Seguridad: credenciales de IA cifradas
|
|
313
328
|
|
|
314
|
-
La consola \`/ai\` y el panel de chat exigen una contrasena local que deriva (PBKDF2) una clave AES-256-GCM; las claves quedan cifradas en \`localStorage\` y solo se descifran durante la sesion (\`@gitpagedocs/tools/security\`).
|
|
329
|
+
La consola \`/ai\` y el panel de chat exigen una contrasena local que deriva (PBKDF2) una clave AES-256-GCM; las claves quedan cifradas en \`localStorage\` y solo se descifran durante la sesion (\`@gitpagedocs/tools/security\`). El panel se bloquea solo tras \`site.AiChatAutoLockSeconds\` segundos sin uso (por defecto 30; \`0\` lo desactiva): un modal centrado con cuenta regresiva, en el idioma seleccionado, permite cancelar o bloquear ahora, y volver exige la contrasena de nuevo. En la CLI, la clave de \`.gitpagedocsconfig\` queda sellada en la boveda cifrada \`.gitpagedocsvault\` y la contrasena de la boveda se pide en cada ejecucion.
|
|
315
330
|
|
|
316
331
|
## Flujo de datos
|
|
317
332
|
|
package/content/docs-pt.mjs
CHANGED
|
@@ -165,6 +165,21 @@ O config da versao pode renderizar um container **Codigo fonte** via \`routes-so
|
|
|
165
165
|
|
|
166
166
|
- \`GITPAGEDOCS_REPOSITORY_SEARCH\` – busca de repositorio (local)
|
|
167
167
|
- \`GITHUB_ACTIONS\` – modo build GitHub Pages
|
|
168
|
+
|
|
169
|
+
## Assistente de IA
|
|
170
|
+
|
|
171
|
+
Os docs trazem um assistente de IA em duas superficies: um **chat drawer** dentro dos docs (botao de chat de IA na barra lateral, ativado por \`site.AiChatEnabled\`) e uma pagina **console \`/ai\`** dedicada.
|
|
172
|
+
|
|
173
|
+
- **14 provedores** em um unico core compartilhado: OpenAI, Anthropic, Gemini, OpenRouter, Ollama, Azure OpenAI, Mistral, DeepSeek, Cohere, Groq, xAI, Together, Fireworks, Perplexity.
|
|
174
|
+
- **Escolha de modelo** — a partir do catalogo de cada provedor (\`gitpagedocs models <provedor>\`); um id de modelo salvo que o provedor aposentou e trocado pelo padrao do provedor automaticamente.
|
|
175
|
+
- **Criptografia em repouso** — sua chave de API e selada com AES-256-GCM atras de uma **senha local** e nunca fica em texto puro nem em logs. Uma chave legada em texto puro e migrada e apagada no primeiro desbloqueio.
|
|
176
|
+
- **Bloqueio por inatividade** — o chat drawer se bloqueia sozinho apos \`site.AiChatAutoLockSeconds\` segundos sem uso (padrao 30, \`0\` desativa): um modal centralizado com contagem regressiva, no seu idioma, permite cancelar ou bloquear agora, e desbloquear pede a senha de novo.
|
|
177
|
+
- **Provedores resilientes** — erros transitorios do provedor sao repetidos (3 tentativas) e uma falha final vira uma mensagem simples terminando em "Tente novamente!".
|
|
178
|
+
- **Geracao de documentacao com IA** — \`gitpagedocs ai\` varre os caminhos escolhidos e escreve markdown multilingue (pt/en/es); reutilizavel via \`.gitpagedocsconfig\`, cuja chave de API fica selada no cofre criptografado \`.gitpagedocsvault\` (a senha do cofre e pedida em toda execucao). \`gitpagedocs chat\` leva o mesmo assistente ao terminal.
|
|
179
|
+
|
|
180
|
+
## Servidor MCP
|
|
181
|
+
|
|
182
|
+
\`gitpagedocs mcp start\` sobe um servidor Model Context Protocol (stdio) expondo **20 tools** (sistema de arquivos, IA, geracao/analise de docs) e **7 resources** (\`project://structure|docs|config|repository|readme|ai/providers|ai/models\`) para editores e agentes de IA.
|
|
168
183
|
`,
|
|
169
184
|
configuration: `# Configuracao
|
|
170
185
|
|
|
@@ -311,7 +326,7 @@ O projeto e organizado por fronteiras de feature e responsabilidades do runtime.
|
|
|
311
326
|
|
|
312
327
|
## Seguranca: credenciais de IA criptografadas
|
|
313
328
|
|
|
314
|
-
O console \`/ai\` e o chat drawer exigem uma senha local que deriva (PBKDF2) uma chave AES-256-GCM; as chaves ficam criptografadas no \`localStorage\` e so sao descriptografadas durante a sessao (\`@gitpagedocs/tools/security\`).
|
|
329
|
+
O console \`/ai\` e o chat drawer exigem uma senha local que deriva (PBKDF2) uma chave AES-256-GCM; as chaves ficam criptografadas no \`localStorage\` e so sao descriptografadas durante a sessao (\`@gitpagedocs/tools/security\`). O drawer se bloqueia sozinho apos \`site.AiChatAutoLockSeconds\` segundos sem uso (padrao 30; \`0\` desativa): um modal centralizado com contagem regressiva, no idioma selecionado, permite cancelar ou bloquear agora, e voltar exige a senha de novo. Na CLI, a chave de \`.gitpagedocsconfig\` fica selada no cofre criptografado \`.gitpagedocsvault\` e a senha do cofre e pedida em toda execucao.
|
|
315
330
|
|
|
316
331
|
## Fluxo de dados
|
|
317
332
|
|
|
@@ -138,6 +138,7 @@ export function getDefaultSiteConfig(projectLink, languageToggles = buildLanguag
|
|
|
138
138
|
audioSequentialPlayback: false,
|
|
139
139
|
audioPopoverHideSource: false,
|
|
140
140
|
audioPopoverShowMinutes: true,
|
|
141
|
+
AiChatAutoLockSeconds: 30,
|
|
141
142
|
audioTracks: [
|
|
142
143
|
{
|
|
143
144
|
url: "https://www.youtube.com/watch?v=xAR6N9N8e6U",
|
package/data/i18n-langmenu.mjs
CHANGED
|
@@ -81,12 +81,17 @@ export const defaultLangMenu = {
|
|
|
81
81
|
aiChatResetPopupDesc: "Isso apaga todas as chaves de API salvas e a senha local. Você criará uma nova senha na próxima vez.",
|
|
82
82
|
aiChatResetConfirmBtn: "Sim, redefinir",
|
|
83
83
|
aiChatResetCancelBtn: "Cancelar",
|
|
84
|
+
aiChatRetryHint: "Tente novamente!",
|
|
85
|
+
aiChatAutoLockTitle: "Bloqueio por inatividade",
|
|
86
|
+
aiChatAutoLockDesc: "Você não usou a IA por um tempo. O chat será bloqueado e suas chaves continuarão criptografadas em {seconds}s.",
|
|
87
|
+
aiChatAutoLockConfirmBtn: "Salvar",
|
|
88
|
+
aiChatAutoLockCancelBtn: "Cancelar",
|
|
84
89
|
aiChatProviderLabel: "Provedor:",
|
|
85
90
|
aiChatApiKeyLabel: "Chave de API (deixe em branco para IA local):",
|
|
86
|
-
aiChatProviderOpenAI: "OpenAI
|
|
87
|
-
aiChatProviderClaude: "Anthropic Claude
|
|
88
|
-
aiChatProviderGemini: "Google Gemini
|
|
89
|
-
aiChatProviderOllama: "Ollama
|
|
91
|
+
aiChatProviderOpenAI: "OpenAI",
|
|
92
|
+
aiChatProviderClaude: "Anthropic Claude",
|
|
93
|
+
aiChatProviderGemini: "Google Gemini",
|
|
94
|
+
aiChatProviderOllama: "Ollama",
|
|
90
95
|
aiChatEmptyPageContent: "Nenhuma página ativa compatível.",
|
|
91
96
|
aiChatNoPageId: "Sem página",
|
|
92
97
|
aiChatOllamaUrlLabel: "URL da API Ollama (deixe em branco para local)",
|
|
@@ -197,12 +202,17 @@ export const defaultLangMenu = {
|
|
|
197
202
|
aiChatResetPopupDesc: "This erases all saved API keys and the local password. You'll create a new password next time.",
|
|
198
203
|
aiChatResetConfirmBtn: "Yes, reset",
|
|
199
204
|
aiChatResetCancelBtn: "Cancel",
|
|
205
|
+
aiChatRetryHint: "Try again!",
|
|
206
|
+
aiChatAutoLockTitle: "Inactivity lock",
|
|
207
|
+
aiChatAutoLockDesc: "You have not used the AI for a while. The chat locks and your keys stay encrypted in {seconds}s.",
|
|
208
|
+
aiChatAutoLockConfirmBtn: "OK",
|
|
209
|
+
aiChatAutoLockCancelBtn: "Cancel",
|
|
200
210
|
aiChatProviderLabel: "Provider:",
|
|
201
211
|
aiChatApiKeyLabel: "API Key (leave blank for Local AI):",
|
|
202
|
-
aiChatProviderOpenAI: "OpenAI
|
|
203
|
-
aiChatProviderClaude: "Anthropic Claude
|
|
204
|
-
aiChatProviderGemini: "Google Gemini
|
|
205
|
-
aiChatProviderOllama: "Ollama
|
|
212
|
+
aiChatProviderOpenAI: "OpenAI",
|
|
213
|
+
aiChatProviderClaude: "Anthropic Claude",
|
|
214
|
+
aiChatProviderGemini: "Google Gemini",
|
|
215
|
+
aiChatProviderOllama: "Ollama",
|
|
206
216
|
aiChatEmptyPageContent: "No compatible active page.",
|
|
207
217
|
aiChatNoPageId: "No page",
|
|
208
218
|
aiChatOllamaUrlLabel: "Ollama API URL (leave blank for local)",
|
|
@@ -313,12 +323,17 @@ export const defaultLangMenu = {
|
|
|
313
323
|
aiChatResetPopupDesc: "Esto borra todas las claves de API guardadas y la contraseña local. Crearás una nueva contraseña la próxima vez.",
|
|
314
324
|
aiChatResetConfirmBtn: "Sí, restablecer",
|
|
315
325
|
aiChatResetCancelBtn: "Cancelar",
|
|
326
|
+
aiChatRetryHint: "¡Inténtalo de nuevo!",
|
|
327
|
+
aiChatAutoLockTitle: "Bloqueo por inactividad",
|
|
328
|
+
aiChatAutoLockDesc: "No has usado la IA por un tiempo. El chat se bloqueará y tus claves seguirán cifradas en {seconds}s.",
|
|
329
|
+
aiChatAutoLockConfirmBtn: "Guardar",
|
|
330
|
+
aiChatAutoLockCancelBtn: "Cancelar",
|
|
316
331
|
aiChatProviderLabel: "Proveedor:",
|
|
317
332
|
aiChatApiKeyLabel: "Clave de API (déjalo en blanco para IA local):",
|
|
318
|
-
aiChatProviderOpenAI: "OpenAI
|
|
319
|
-
aiChatProviderClaude: "Anthropic Claude
|
|
320
|
-
aiChatProviderGemini: "Google Gemini
|
|
321
|
-
aiChatProviderOllama: "Ollama
|
|
333
|
+
aiChatProviderOpenAI: "OpenAI",
|
|
334
|
+
aiChatProviderClaude: "Anthropic Claude",
|
|
335
|
+
aiChatProviderGemini: "Google Gemini",
|
|
336
|
+
aiChatProviderOllama: "Ollama",
|
|
322
337
|
aiChatEmptyPageContent: "Ninguna página activa compatible.",
|
|
323
338
|
aiChatNoPageId: "Sin página",
|
|
324
339
|
aiChatOllamaUrlLabel: "URL de la API de Ollama (déjalo en blanco para local)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gitpagedocs/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "CLI that scaffolds and maintains gitpagedocs documentation, generates docs with AI, configures GitHub Pages, and runs the MCP server.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"gitpagedocs": "index.mjs"
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@clack/prompts": "^1.5.1",
|
|
32
|
-
"@gitpagedocs/mcp": "^0.0.
|
|
33
|
-
"@gitpagedocs/tools": "^0.0.
|
|
32
|
+
"@gitpagedocs/mcp": "^0.0.3",
|
|
33
|
+
"@gitpagedocs/tools": "^0.0.3",
|
|
34
34
|
"tsx": "^4.21.0"
|
|
35
35
|
},
|
|
36
36
|
"repository": {
|
|
@@ -3,7 +3,11 @@ import { createDefaultFactory } from "@gitpagedocs/tools/ai";
|
|
|
3
3
|
import { PROVIDER_CATALOG } from "@gitpagedocs/tools";
|
|
4
4
|
import type { AIProvider, ProviderConfig } from "@gitpagedocs/tools/ports";
|
|
5
5
|
import { ChatSession } from "../../ai/core/chat-session";
|
|
6
|
+
import { createStoredKeyUnlocker } from "../../ai/application/ai-credentials";
|
|
6
7
|
import { resolveChatCredentials, type ResolvedChatCredentials } from "../../ai/application/resolve-chat-credentials";
|
|
8
|
+
import { AiConfigFileRepository } from "../../ai/infrastructure/ai-config-file";
|
|
9
|
+
import { AiKeyVault } from "../../ai/infrastructure/ai-key-vault";
|
|
10
|
+
import { clackVaultPasswordPrompt } from "../../ai/presentation/vault-prompts";
|
|
7
11
|
import {
|
|
8
12
|
ElapsedSpinner,
|
|
9
13
|
accent,
|
|
@@ -215,10 +219,20 @@ function printSetupHelp(providerHint: string): void {
|
|
|
215
219
|
*/
|
|
216
220
|
export async function runChat(ctx: CommandContext): Promise<void> {
|
|
217
221
|
const flags = parseChatArgs(ctx.args);
|
|
222
|
+
const configRepo = new AiConfigFileRepository({ cwd: ctx.cwd });
|
|
218
223
|
const creds: ResolvedChatCredentials | null = await resolveChatCredentials({
|
|
219
224
|
cwd: ctx.cwd,
|
|
220
225
|
providerOverride: flags.provider,
|
|
221
226
|
modelOverride: flags.model,
|
|
227
|
+
configRepo,
|
|
228
|
+
// The stored key is sealed in the vault: the password is asked on every run
|
|
229
|
+
// (or read from GITPAGEDOCS_VAULT_PASSWORD when there is no terminal).
|
|
230
|
+
unlockStoredKey: createStoredKeyUnlocker({
|
|
231
|
+
configRepo,
|
|
232
|
+
vault: new AiKeyVault(),
|
|
233
|
+
prompt: process.stdin.isTTY ? clackVaultPasswordPrompt : undefined,
|
|
234
|
+
onInfo: (message) => writeChrome(danger(message)),
|
|
235
|
+
}),
|
|
222
236
|
});
|
|
223
237
|
|
|
224
238
|
if (!creds) {
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { AiConfigFileRepository } from "../../ai/infrastructure/ai-config-file";
|
|
2
|
+
import { AiKeyVault } from "../../ai/infrastructure/ai-key-vault";
|
|
2
3
|
import type { CommandContext } from "./run-command";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* `gitpagedocs config clear` — delete the stored `.gitpagedocsconfig` from the
|
|
6
|
-
* per-user config directory and any legacy repo-root copy
|
|
7
|
-
*
|
|
7
|
+
* per-user config directory (and any legacy repo-root copy) together with the
|
|
8
|
+
* encrypted key vault, wiping the saved AI credentials, provider and scan paths.
|
|
8
9
|
*/
|
|
9
10
|
export async function runConfigClear(ctx: CommandContext): Promise<void> {
|
|
10
11
|
const repository = new AiConfigFileRepository({ cwd: ctx.cwd });
|
|
12
|
+
const vault = new AiKeyVault();
|
|
11
13
|
const removed = await repository.clear();
|
|
14
|
+
if (await vault.clear()) removed.push(vault.getVaultPath());
|
|
12
15
|
|
|
13
16
|
if (!removed.length) {
|
|
14
17
|
// eslint-disable-next-line no-console
|