@ggui-ai/cli 0.1.0-rc.1

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 (117) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +96 -0
  3. package/dist/agent-resolution.d.ts +41 -0
  4. package/dist/agent-resolution.d.ts.map +1 -0
  5. package/dist/agent-resolution.js +80 -0
  6. package/dist/api-client.d.ts +59 -0
  7. package/dist/api-client.d.ts.map +1 -0
  8. package/dist/api-client.js +145 -0
  9. package/dist/auth-keys.d.ts +10 -0
  10. package/dist/auth-keys.d.ts.map +1 -0
  11. package/dist/auth-keys.js +382 -0
  12. package/dist/auth-login.d.ts +11 -0
  13. package/dist/auth-login.d.ts.map +1 -0
  14. package/dist/auth-login.js +231 -0
  15. package/dist/auth-logout.d.ts +3 -0
  16. package/dist/auth-logout.d.ts.map +1 -0
  17. package/dist/auth-logout.js +32 -0
  18. package/dist/auth-store.d.ts +67 -0
  19. package/dist/auth-store.d.ts.map +1 -0
  20. package/dist/auth-store.js +150 -0
  21. package/dist/auth-whoami.d.ts +3 -0
  22. package/dist/auth-whoami.d.ts.map +1 -0
  23. package/dist/auth-whoami.js +52 -0
  24. package/dist/blueprint-command.d.ts +3 -0
  25. package/dist/blueprint-command.d.ts.map +1 -0
  26. package/dist/blueprint-command.js +203 -0
  27. package/dist/blueprint-create.d.ts +71 -0
  28. package/dist/blueprint-create.d.ts.map +1 -0
  29. package/dist/blueprint-create.js +377 -0
  30. package/dist/byok-resolver.d.ts +141 -0
  31. package/dist/byok-resolver.d.ts.map +1 -0
  32. package/dist/byok-resolver.js +89 -0
  33. package/dist/cli.d.ts +3 -0
  34. package/dist/cli.d.ts.map +1 -0
  35. package/dist/cli.js +1066 -0
  36. package/dist/dev-command.d.ts +169 -0
  37. package/dist/dev-command.d.ts.map +1 -0
  38. package/dist/dev-command.js +222 -0
  39. package/dist/gadget-command.d.ts +3 -0
  40. package/dist/gadget-command.d.ts.map +1 -0
  41. package/dist/gadget-command.js +181 -0
  42. package/dist/gadget-create.d.ts +106 -0
  43. package/dist/gadget-create.d.ts.map +1 -0
  44. package/dist/gadget-create.js +492 -0
  45. package/dist/generation-probe.d.ts +135 -0
  46. package/dist/generation-probe.d.ts.map +1 -0
  47. package/dist/generation-probe.js +132 -0
  48. package/dist/internal/artifact-install.d.ts +150 -0
  49. package/dist/internal/artifact-install.d.ts.map +1 -0
  50. package/dist/internal/artifact-install.js +1019 -0
  51. package/dist/internal/artifact-publish.d.ts +204 -0
  52. package/dist/internal/artifact-publish.d.ts.map +1 -0
  53. package/dist/internal/artifact-publish.js +1141 -0
  54. package/dist/internal/artifact-search.d.ts +155 -0
  55. package/dist/internal/artifact-search.d.ts.map +1 -0
  56. package/dist/internal/artifact-search.js +539 -0
  57. package/dist/internal/artifact-uninstall.d.ts +30 -0
  58. package/dist/internal/artifact-uninstall.d.ts.map +1 -0
  59. package/dist/internal/artifact-uninstall.js +205 -0
  60. package/dist/internal/auth-cache.d.ts +40 -0
  61. package/dist/internal/auth-cache.d.ts.map +1 -0
  62. package/dist/internal/auth-cache.js +133 -0
  63. package/dist/internal/auth-strategy.d.ts +89 -0
  64. package/dist/internal/auth-strategy.d.ts.map +1 -0
  65. package/dist/internal/auth-strategy.js +132 -0
  66. package/dist/internal/ggui-json.d.ts +53 -0
  67. package/dist/internal/ggui-json.d.ts.map +1 -0
  68. package/dist/internal/ggui-json.js +92 -0
  69. package/dist/internal/key-store.d.ts +43 -0
  70. package/dist/internal/key-store.d.ts.map +1 -0
  71. package/dist/internal/key-store.js +128 -0
  72. package/dist/internal/oidc-token.d.ts +52 -0
  73. package/dist/internal/oidc-token.d.ts.map +1 -0
  74. package/dist/internal/oidc-token.js +351 -0
  75. package/dist/internal/register-author-key.d.ts +44 -0
  76. package/dist/internal/register-author-key.d.ts.map +1 -0
  77. package/dist/internal/register-author-key.js +212 -0
  78. package/dist/local-keys.d.ts +47 -0
  79. package/dist/local-keys.d.ts.map +1 -0
  80. package/dist/local-keys.js +115 -0
  81. package/dist/mcp-backend.d.ts +356 -0
  82. package/dist/mcp-backend.d.ts.map +1 -0
  83. package/dist/mcp-backend.js +668 -0
  84. package/dist/paths.d.ts +77 -0
  85. package/dist/paths.d.ts.map +1 -0
  86. package/dist/paths.js +119 -0
  87. package/dist/persistent-secrets.d.ts +28 -0
  88. package/dist/persistent-secrets.d.ts.map +1 -0
  89. package/dist/persistent-secrets.js +94 -0
  90. package/dist/persistent-short-code-index.d.ts +14 -0
  91. package/dist/persistent-short-code-index.d.ts.map +1 -0
  92. package/dist/persistent-short-code-index.js +39 -0
  93. package/dist/persistent-stores.d.ts +12 -0
  94. package/dist/persistent-stores.d.ts.map +1 -0
  95. package/dist/persistent-stores.js +42 -0
  96. package/dist/serve-command.d.ts +500 -0
  97. package/dist/serve-command.d.ts.map +1 -0
  98. package/dist/serve-command.js +616 -0
  99. package/dist/serve-fallback.d.ts +64 -0
  100. package/dist/serve-fallback.d.ts.map +1 -0
  101. package/dist/serve-fallback.js +122 -0
  102. package/dist/spawn-cloudflared.d.ts +61 -0
  103. package/dist/spawn-cloudflared.d.ts.map +1 -0
  104. package/dist/spawn-cloudflared.js +167 -0
  105. package/dist/theme-command.d.ts +4 -0
  106. package/dist/theme-command.d.ts.map +1 -0
  107. package/dist/theme-command.js +154 -0
  108. package/dist/theme-file-uploader.d.ts +8 -0
  109. package/dist/theme-file-uploader.d.ts.map +1 -0
  110. package/dist/theme-file-uploader.js +58 -0
  111. package/dist/theme-writer.d.ts +8 -0
  112. package/dist/theme-writer.d.ts.map +1 -0
  113. package/dist/theme-writer.js +103 -0
  114. package/dist/tunnel-provider.d.ts +273 -0
  115. package/dist/tunnel-provider.d.ts.map +1 -0
  116. package/dist/tunnel-provider.js +142 -0
  117. package/package.json +81 -0
@@ -0,0 +1,377 @@
1
+ /**
2
+ * `ggui blueprint create <scope/name>` — scaffold a new blueprint repo.
3
+ *
4
+ * Sibling of `gadget-create.ts`. Blueprints differ from gadgets in two
5
+ * ways that justify a separate scaffolder (rather than a
6
+ * kind-discriminated single template):
7
+ *
8
+ * - **Source body** — blueprints are TSX components, not bundled
9
+ * hook libraries. The scaffold writes a `src/blueprint.tsx` with
10
+ * a minimal React component + a `src/contract.ts` stub that
11
+ * declares a `DataContract` for the LLM-driven cache-match path.
12
+ * - **Manifest shape** — `ggui.blueprint.json` carries `source`,
13
+ * `contract`, `fixtureProps`, `variance`. The scaffold synthesizes
14
+ * a working manifest with the inline source body, so an author can
15
+ * `cd <dir> && ggui blueprint publish` immediately after generation.
16
+ *
17
+ * The scaffold validates the synthesized manifest with
18
+ * `parseBlueprintManifest` before any write — template regressions
19
+ * fail LOUD at the scaffolder rather than silently shipping a broken
20
+ * `ggui.blueprint.json`.
21
+ *
22
+ * Kept pure / testable — no `process.exit`, no direct stdout writes.
23
+ * `blueprint-command.ts` composes this with the real argv + writes
24
+ * the banner.
25
+ */
26
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
27
+ import { join, resolve } from 'node:path';
28
+ import { GGUI_BLUEPRINT_JSON_FILENAME, parseBlueprintManifest, } from '@ggui-ai/artifact-manifest';
29
+ import { parseScopeName } from './gadget-create.js';
30
+ /** Default version for a freshly scaffolded blueprint. */
31
+ const DEFAULT_VERSION = '0.0.1';
32
+ /** Default visibility — public matches the marketplace's primary path. */
33
+ const DEFAULT_VISIBILITY = 'public';
34
+ /**
35
+ * Parse the `ggui blueprint create` flag tail. Returns a discriminated
36
+ * `{error}` vs. successful flags so the caller doesn't throw on user
37
+ * input.
38
+ */
39
+ export function parseBlueprintCreateFlags(args) {
40
+ if (args.length === 0) {
41
+ return { error: '__help__' };
42
+ }
43
+ let scopeName;
44
+ let dir;
45
+ let description;
46
+ let visibility;
47
+ for (let i = 0; i < args.length; i++) {
48
+ const arg = args[i];
49
+ if (arg === '--help' || arg === '-h')
50
+ return { error: '__help__' };
51
+ if (arg === '--dir') {
52
+ const value = args[i + 1];
53
+ if (typeof value !== 'string' || value.length === 0) {
54
+ return { error: '--dir requires a value' };
55
+ }
56
+ dir = value;
57
+ i += 1;
58
+ continue;
59
+ }
60
+ if (arg === '--description') {
61
+ const value = args[i + 1];
62
+ if (typeof value !== 'string' || value.length === 0) {
63
+ return { error: '--description requires a value' };
64
+ }
65
+ description = value;
66
+ i += 1;
67
+ continue;
68
+ }
69
+ if (arg === '--visibility') {
70
+ const value = args[i + 1];
71
+ if (value !== 'public' && value !== 'private') {
72
+ return { error: '--visibility must be "public" or "private"' };
73
+ }
74
+ visibility = value;
75
+ i += 1;
76
+ continue;
77
+ }
78
+ if (arg.startsWith('--')) {
79
+ return { error: `unknown flag: ${arg}` };
80
+ }
81
+ if (scopeName === undefined) {
82
+ scopeName = arg;
83
+ continue;
84
+ }
85
+ return { error: `unexpected positional argument: ${arg}` };
86
+ }
87
+ if (scopeName === undefined) {
88
+ return { error: 'missing required <scope/name> positional argument' };
89
+ }
90
+ const flags = {
91
+ scopeName,
92
+ ...(dir !== undefined ? { dir } : {}),
93
+ ...(description !== undefined ? { description } : {}),
94
+ ...(visibility !== undefined ? { visibility } : {}),
95
+ };
96
+ return { flags };
97
+ }
98
+ /**
99
+ * Build the minimal TSX source body the scaffold ships as
100
+ * `src/blueprint.tsx`. Authors edit this in-place; once the source
101
+ * stabilizes the publish flow re-reads + inlines it into the manifest's
102
+ * `source` field at upload time.
103
+ */
104
+ function buildBlueprintTsx(parsed) {
105
+ const componentName = toPascalCase(parsed.name);
106
+ return `/**
107
+ * ${parsed.scope}/${parsed.name} — ggui blueprint.
108
+ *
109
+ * Default-exports a React component the runtime renders against the
110
+ * declared DataContract. Replace this placeholder with your real UI.
111
+ */
112
+ export interface ${componentName}Props {
113
+ readonly title?: string;
114
+ }
115
+
116
+ export default function ${componentName}(props: ${componentName}Props): JSX.Element {
117
+ return (
118
+ <section data-blueprint="${parsed.scope}/${parsed.name}">
119
+ <h1>{props.title ?? '${parsed.scope}/${parsed.name}'}</h1>
120
+ </section>
121
+ );
122
+ }
123
+ `;
124
+ }
125
+ /**
126
+ * Build the matching `src/contract.ts` stub — a minimal `DataContract`
127
+ * the blueprint registers under. Authors flesh out `propsSchema` to
128
+ * match the component's prop shape and add `clientCapabilities` /
129
+ * `commitSpec` as needed.
130
+ */
131
+ function buildContractStub(parsed) {
132
+ return `/**
133
+ * DataContract for ${parsed.scope}/${parsed.name}. The runtime feeds
134
+ * the matching props into the default-exported component on render;
135
+ * the contract's hash also drives cache-match for the cold-gen short-
136
+ * circuit.
137
+ */
138
+ export const contract = {
139
+ schemaVersion: '1' as const,
140
+ contextSpec: {
141
+ propsSchema: {
142
+ type: 'object',
143
+ properties: {
144
+ title: { type: 'string' },
145
+ },
146
+ },
147
+ },
148
+ } as const;
149
+ `;
150
+ }
151
+ /** Convert a kebab-case blueprint name → PascalCase component name. */
152
+ function toPascalCase(name) {
153
+ return name
154
+ .split('-')
155
+ .filter((s) => s.length > 0)
156
+ .map((s) => s[0].toUpperCase() + s.slice(1))
157
+ .join('');
158
+ }
159
+ /**
160
+ * Build + validate the {@link BlueprintManifest} BEFORE any write.
161
+ * A template regression that drifts (e.g. drops `source`) blows up
162
+ * here, before any file lands on disk, so authors don't see a corrupt
163
+ * scaffold.
164
+ */
165
+ export function buildScaffoldManifest(args) {
166
+ const description = args.description ??
167
+ `${args.scope}/${args.name} — ggui marketplace blueprint.`;
168
+ const raw = {
169
+ kind: 'blueprint',
170
+ scope: args.scope,
171
+ name: args.name,
172
+ version: DEFAULT_VERSION,
173
+ visibility: args.visibility,
174
+ source: args.source,
175
+ description,
176
+ tags: [],
177
+ };
178
+ return parseBlueprintManifest(raw);
179
+ }
180
+ function buildPackageJson(args) {
181
+ return {
182
+ name: `${args.scope}/${args.name}`,
183
+ version: DEFAULT_VERSION,
184
+ ...(args.description !== undefined ? { description: args.description } : {}),
185
+ type: 'module',
186
+ files: ['src', GGUI_BLUEPRINT_JSON_FILENAME, 'README.md'],
187
+ devDependencies: {
188
+ typescript: '^5.0.0',
189
+ '@types/react': '^19.0.0',
190
+ },
191
+ };
192
+ }
193
+ function buildReadme(args) {
194
+ const summary = args.description ??
195
+ `${args.scope}/${args.name} — ggui marketplace blueprint.`;
196
+ return `# ${args.scope}/${args.name}
197
+
198
+ ${summary}
199
+
200
+ ## Develop
201
+
202
+ \`\`\`bash
203
+ pnpm install
204
+ \`\`\`
205
+
206
+ Edit \`src/blueprint.tsx\` (component body) and \`src/contract.ts\`
207
+ (matching \`DataContract\`).
208
+
209
+ ## Publish
210
+
211
+ \`\`\`bash
212
+ ggui blueprint publish
213
+ \`\`\`
214
+
215
+ See [ggui blueprint docs](https://ggui.ai/docs/blueprints) for the full
216
+ marketplace flow.
217
+ `;
218
+ }
219
+ /** Refuse-to-overwrite check, matching gadget-create's posture. */
220
+ async function isDirectoryEmpty(path) {
221
+ try {
222
+ const entries = await readdir(path);
223
+ return { exists: true, empty: entries.length === 0 };
224
+ }
225
+ catch (err) {
226
+ if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
227
+ return { exists: false, empty: true };
228
+ }
229
+ throw err;
230
+ }
231
+ }
232
+ /**
233
+ * Run the blueprint scaffold end-to-end. Resolves the target dir,
234
+ * builds + validates the manifest, writes files. Returns a structured
235
+ * result so the CLI driver can render copy + exit code without
236
+ * re-deriving paths.
237
+ */
238
+ export async function runBlueprintCreate(flags, options) {
239
+ const parsed = parseScopeName(flags.scopeName);
240
+ if (!parsed) {
241
+ return {
242
+ ok: false,
243
+ code: 'invalid-scope-name',
244
+ message: `"${flags.scopeName}" is not a valid <scope>/<name>. Expected \`@org/blueprint-name\` (e.g. \`@my-org/login-form\`).`,
245
+ };
246
+ }
247
+ // Blueprint name regex (`^[a-z0-9][a-z0-9_-]{0,63}$`) is enforced by
248
+ // the manifest schema; a structurally-valid scope/name with an
249
+ // invalid blueprint slug surfaces at parse time below as
250
+ // `manifest-invalid`. Bail early on the more obvious mismatches so
251
+ // the operator sees a crisp message.
252
+ if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(parsed.name)) {
253
+ return {
254
+ ok: false,
255
+ code: 'invalid-name',
256
+ message: `"${parsed.name}" is not a valid blueprint name. Expected lowercase alphanum + hyphens / underscores, 1..64 chars, first char alphanumeric (e.g. \`login-form\`).`,
257
+ };
258
+ }
259
+ const { scope, name } = parsed;
260
+ const visibility = flags.visibility ?? DEFAULT_VISIBILITY;
261
+ const source = buildBlueprintTsx(parsed);
262
+ let manifest;
263
+ try {
264
+ manifest = buildScaffoldManifest({
265
+ scope,
266
+ name,
267
+ visibility,
268
+ source,
269
+ ...(flags.description !== undefined
270
+ ? { description: flags.description }
271
+ : {}),
272
+ });
273
+ }
274
+ catch (err) {
275
+ return {
276
+ ok: false,
277
+ code: 'manifest-invalid',
278
+ message: err instanceof Error ? err.message : 'failed to validate manifest',
279
+ };
280
+ }
281
+ // `resolve` (not `join`): when `flags.dir` is an absolute path, it must
282
+ // win over `options.cwd`. `join('/repo', '/tmp/foo')` concatenates to
283
+ // `/repo/tmp/foo`; `resolve('/repo', '/tmp/foo')` correctly returns
284
+ // `/tmp/foo`. Relative paths still resolve against cwd.
285
+ const targetDir = resolve(options.cwd, flags.dir ?? name);
286
+ let dirState;
287
+ try {
288
+ dirState = await isDirectoryEmpty(targetDir);
289
+ }
290
+ catch (err) {
291
+ return {
292
+ ok: false,
293
+ code: 'fs-error',
294
+ message: err instanceof Error ? err.message : 'failed to read target dir',
295
+ };
296
+ }
297
+ if (dirState.exists && !dirState.empty) {
298
+ return {
299
+ ok: false,
300
+ code: 'target-not-empty',
301
+ message: `target directory ${targetDir} is not empty. Refusing to overwrite — choose a different --dir or remove existing files.`,
302
+ };
303
+ }
304
+ try {
305
+ await mkdir(targetDir, { recursive: true });
306
+ await mkdir(join(targetDir, 'src'), { recursive: true });
307
+ const manifestPath = join(targetDir, GGUI_BLUEPRINT_JSON_FILENAME);
308
+ await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf-8');
309
+ const pkg = buildPackageJson({
310
+ scope,
311
+ name,
312
+ ...(flags.description !== undefined
313
+ ? { description: flags.description }
314
+ : {}),
315
+ });
316
+ await writeFile(join(targetDir, 'package.json'), `${JSON.stringify(pkg, null, 2)}\n`, 'utf-8');
317
+ await writeFile(join(targetDir, 'src', 'blueprint.tsx'), source, 'utf-8');
318
+ await writeFile(join(targetDir, 'src', 'contract.ts'), buildContractStub(parsed), 'utf-8');
319
+ await writeFile(join(targetDir, 'README.md'), buildReadme({
320
+ scope,
321
+ name,
322
+ ...(flags.description !== undefined
323
+ ? { description: flags.description }
324
+ : {}),
325
+ }), 'utf-8');
326
+ }
327
+ catch (err) {
328
+ return {
329
+ ok: false,
330
+ code: 'fs-error',
331
+ message: err instanceof Error ? err.message : 'failed to write files',
332
+ };
333
+ }
334
+ return {
335
+ ok: true,
336
+ targetDir,
337
+ files: [
338
+ GGUI_BLUEPRINT_JSON_FILENAME,
339
+ 'package.json',
340
+ 'src/blueprint.tsx',
341
+ 'src/contract.ts',
342
+ 'README.md',
343
+ ],
344
+ manifest,
345
+ };
346
+ }
347
+ export const BLUEPRINT_CREATE_HELP = `ggui blueprint create — scaffold a new blueprint repo
348
+
349
+ Usage:
350
+ ggui blueprint create <scope/name> [options]
351
+
352
+ Arguments:
353
+ <scope/name> Required. \`@my-org/login-form\` style identifier.
354
+ The scope (with leading \`@\`) becomes the manifest's
355
+ \`scope\`; the segment after \`/\` becomes the \`name\`
356
+ (must match \`^[a-z0-9][a-z0-9_-]{0,63}$\`).
357
+
358
+ Options:
359
+ --dir <path> Target directory. Defaults to \`./<name>\`.
360
+ --description <text> One-line description threaded into the
361
+ manifest + README.
362
+ --visibility <p> \`public\` (default, sigstore-signed) or
363
+ \`private\` (Ed25519-signed, org-only).
364
+ --help, -h Show this help.
365
+
366
+ Output:
367
+ <dir>/ggui.blueprint.json Strict-schema manifest (validated before write).
368
+ <dir>/package.json Workspace shell.
369
+ <dir>/src/blueprint.tsx Default-exported React component stub.
370
+ <dir>/src/contract.ts Matching \`DataContract\` stub.
371
+ <dir>/README.md One-paragraph orientation.
372
+
373
+ After scaffold:
374
+ cd <dir> && pnpm install
375
+ # implement the component in src/blueprint.tsx, then:
376
+ ggui blueprint publish # publishes to the resolved registry.
377
+ `;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * BYOK ("bring your own key") provider-key resolver — the `ggui` CLI's
3
+ * personal-mode credential plumbing for local UI generation.
4
+ *
5
+ * Resolves a {@link ProviderKeyRef} for a given LLM provider through a
6
+ * deterministic two-step lookup. Used by `buildMcpServerBackend` to
7
+ * decide whether to call a real provider or fail fast with the
8
+ * canonical `'no-credentials'` error.
9
+ *
10
+ * **Resolution order, locked:**
11
+ *
12
+ * 1. **Environment variables** — checked first, always win. The
13
+ * env-var names match the conventions every Node ecosystem uses
14
+ * so operators who already export them for other tools (Claude
15
+ * Code, OpenAI CLI, etc.) get OSS generation working with zero
16
+ * extra config:
17
+ *
18
+ * - `anthropic` → `ANTHROPIC_API_KEY`
19
+ * - `google` → `GOOGLE_API_KEY`, then `GEMINI_API_KEY`
20
+ * (alias — Google publishes both)
21
+ * - `openai` → `OPENAI_API_KEY`
22
+ * - `openrouter` → `OPENROUTER_API_KEY`
23
+ * - `bedrock` → not env-resolvable. Bedrock auth flows
24
+ * through `AWS_*` env / IAM role chain at
25
+ * the SDK level; the resolver returns null
26
+ * and the (future) bedrock adapter pulls
27
+ * from the standard AWS credential chain.
28
+ *
29
+ * 2. **`~/.ggui/credentials.json`** — operator-persistent personal
30
+ * store. Backed by `PlaintextFileProviderKeyStore` from
31
+ * `@ggui-ai/mcp-server-core/plaintext` at the **global** app
32
+ * scope. Single-user, single-tenant; multi-app or multi-user
33
+ * deployments swap in a different `ProviderKeyStore` binding at
34
+ * composition time and bypass this resolver entirely.
35
+ *
36
+ * **Explicitly NOT a source:** `ggui.json#secrets` or any other
37
+ * project-file location. Plaintext secrets in a project file that
38
+ * gets committed is a footgun, called out in the OSS-split plan
39
+ * (§2.A). This resolver never reads project config.
40
+ *
41
+ * **Failure mode:** missing key returns `null`. Callers map `null` to
42
+ * the canonical `ProviderError{kind:'no-credentials'}` from
43
+ * `@ggui-ai/ui-gen/provider-adapter` so OSS surfaces one consistent
44
+ * structured failure shape regardless of whether the operator never
45
+ * configured a key vs. configured the wrong provider.
46
+ */
47
+ import type { LlmProvider, ProviderKeyStore } from '@ggui-ai/mcp-server-core';
48
+ /**
49
+ * Per-process env interface. Narrow enough that tests can pass a
50
+ * plain object instead of mutating `process.env` globally — vitest
51
+ * workers share `process.env` so a clean injection seam matters.
52
+ */
53
+ export type EnvLike = Readonly<Record<string, string | undefined>>;
54
+ /** App-scope key under which the resolver reads + writes the store. */
55
+ export declare const BYOK_GLOBAL_APP_SCOPE = "global";
56
+ /**
57
+ * Provider → ordered list of env-var names to check. First non-empty
58
+ * wins. Single-name tuple for most providers; google has the
59
+ * `GOOGLE_API_KEY` / `GEMINI_API_KEY` alias pair.
60
+ *
61
+ * `bedrock` is intentionally absent — its auth chain lives outside
62
+ * this resolver (see module JSDoc).
63
+ */
64
+ export declare const PROVIDER_ENV_NAMES: Readonly<Record<Exclude<LlmProvider, 'bedrock'>, readonly string[]>>;
65
+ /**
66
+ * Successful resolution. The `source` field is load-bearing — it
67
+ * lets callers surface "BYOK from env" vs. "BYOK from credentials
68
+ * file" in operator-facing logs (banner / `ggui auth status` later)
69
+ * AND lets tests assert the precedence rule deterministically.
70
+ */
71
+ export interface ByokKeyResolution {
72
+ readonly key: string;
73
+ readonly source: 'env' | 'credentials-file';
74
+ readonly provider: LlmProvider;
75
+ /**
76
+ * Which env-var name produced the key (for `source: 'env'`). Lets
77
+ * the operator-facing banner say "GEMINI_API_KEY" specifically when
78
+ * that's what fired, instead of a generic "google env".
79
+ */
80
+ readonly envName?: string;
81
+ }
82
+ export interface ByokResolverOptions {
83
+ /**
84
+ * Env source. Defaults to `process.env`. Tests pass a plain object
85
+ * so they don't have to mutate `process.env` (which leaks across
86
+ * vitest's shared worker pool).
87
+ */
88
+ readonly env?: EnvLike;
89
+ /**
90
+ * Override the credentials-file backed store. Defaults to a
91
+ * `PlaintextFileProviderKeyStore` reading {@link getCredentialsFile}
92
+ * — i.e. `~/.ggui/credentials.json` (or `$GGUI_CONFIG_DIR/credentials.json`
93
+ * when set). Production callers leave this unset; tests inject a
94
+ * fixture store or `null` to disable the file leg.
95
+ *
96
+ * Pass `null` (NOT `undefined`) to explicitly disable the file
97
+ * lookup. `undefined` triggers the default-store construction.
98
+ */
99
+ readonly fileStore?: ProviderKeyStore | null;
100
+ }
101
+ /**
102
+ * Per-call resolver options. Threaded through the request → handler →
103
+ * generator chain so the BYOK lookup at gen-time honors the request's
104
+ * authenticated identity.
105
+ */
106
+ export interface ByokResolveOptions {
107
+ /**
108
+ * The end-user scope to consult AFTER the operator's platform key
109
+ * (env + credentials-file at scope='global') has been checked and
110
+ * missed. Operator-first fallback: if the operator set a key, every
111
+ * user uses it (operator pays). If the operator opted out, each
112
+ * user's own key (stored at this scope) is consulted.
113
+ *
114
+ * Absent or equal to `'global'`: no per-user fallback — only env +
115
+ * global file are checked. This is the personal-mode default.
116
+ *
117
+ * Production callers thread the request's authenticated identity
118
+ * here (e.g. `userId` for `kind:'user'`, `appId` for `kind:'app'`).
119
+ */
120
+ readonly userScope?: string;
121
+ }
122
+ /**
123
+ * Wire-level resolver type. Returned by {@link createByokResolver};
124
+ * shaped so call sites can DI a fake without importing the class.
125
+ */
126
+ export interface ByokResolver {
127
+ resolve(provider: LlmProvider, opts?: ByokResolveOptions): Promise<ByokKeyResolution | null>;
128
+ }
129
+ /**
130
+ * Construct a {@link ByokResolver} bound to a specific env + file
131
+ * store. Returned object is stateless; one resolver instance can
132
+ * serve many lookups across a process.
133
+ *
134
+ * The resolver never throws. Missing key → `null`. Malformed
135
+ * credentials file → propagates the underlying file store's error
136
+ * (today: `PlaintextFileProviderKeyStore` throws a "not a valid v1
137
+ * document" error so the operator gets a clear signal instead of a
138
+ * silent miss).
139
+ */
140
+ export declare function createByokResolver(opts?: ByokResolverOptions): ByokResolver;
141
+ //# sourceMappingURL=byok-resolver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"byok-resolver.d.ts","sourceRoot":"","sources":["../src/byok-resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6CG;AACH,OAAO,KAAK,EACV,WAAW,EAEX,gBAAgB,EACjB,MAAM,0BAA0B,CAAC;AAIlC;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;AAEnE,uEAAuE;AACvE,eAAO,MAAM,qBAAqB,WAAW,CAAC;AAE9C;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,EAAE,QAAQ,CACvC,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC,CAM3D,CAAC;AAEF;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,KAAK,GAAG,kBAAkB,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC;IAC/B;;;;OAIG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,mBAAmB;IAClC;;;;OAIG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;;;;;OASG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;CAC9C;AAED;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,CACL,QAAQ,EAAE,WAAW,EACrB,IAAI,CAAC,EAAE,kBAAkB,GACxB,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC,CAAC;CACtC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,GAAE,mBAAwB,GAC7B,YAAY,CA4Ed"}
@@ -0,0 +1,89 @@
1
+ import { PlaintextFileProviderKeyStore } from '@ggui-ai/mcp-server-core/plaintext';
2
+ import { getCredentialsFile } from './paths.js';
3
+ /** App-scope key under which the resolver reads + writes the store. */
4
+ export const BYOK_GLOBAL_APP_SCOPE = 'global';
5
+ /**
6
+ * Provider → ordered list of env-var names to check. First non-empty
7
+ * wins. Single-name tuple for most providers; google has the
8
+ * `GOOGLE_API_KEY` / `GEMINI_API_KEY` alias pair.
9
+ *
10
+ * `bedrock` is intentionally absent — its auth chain lives outside
11
+ * this resolver (see module JSDoc).
12
+ */
13
+ export const PROVIDER_ENV_NAMES = {
14
+ anthropic: ['ANTHROPIC_API_KEY'],
15
+ google: ['GOOGLE_API_KEY', 'GEMINI_API_KEY'],
16
+ openai: ['OPENAI_API_KEY'],
17
+ openrouter: ['OPENROUTER_API_KEY'],
18
+ };
19
+ /**
20
+ * Construct a {@link ByokResolver} bound to a specific env + file
21
+ * store. Returned object is stateless; one resolver instance can
22
+ * serve many lookups across a process.
23
+ *
24
+ * The resolver never throws. Missing key → `null`. Malformed
25
+ * credentials file → propagates the underlying file store's error
26
+ * (today: `PlaintextFileProviderKeyStore` throws a "not a valid v1
27
+ * document" error so the operator gets a clear signal instead of a
28
+ * silent miss).
29
+ */
30
+ export function createByokResolver(opts = {}) {
31
+ const env = opts.env ?? process.env;
32
+ const fileStore = opts.fileStore !== undefined
33
+ ? opts.fileStore
34
+ : new PlaintextFileProviderKeyStore({ filename: getCredentialsFile() });
35
+ // File-store helper. Used twice in the fallback chain (once at the
36
+ // operator's 'global' scope, optionally once at the end-user's
37
+ // identity scope).
38
+ const fileLookupAtScope = async (scope, provider) => {
39
+ if (fileStore === null)
40
+ return null;
41
+ const ref = await fileStore.get(scope, provider);
42
+ if (!ref)
43
+ return null;
44
+ return {
45
+ key: ref.key,
46
+ source: 'credentials-file',
47
+ provider,
48
+ };
49
+ };
50
+ return {
51
+ async resolve(provider, resolveOpts) {
52
+ // Step 1: env. The PROVIDER_ENV_NAMES table is the locked
53
+ // mapping; bedrock isn't in it on purpose, so destructuring to
54
+ // `undefined` gives us the "skip env" path automatically.
55
+ const envNames = provider === 'bedrock'
56
+ ? undefined
57
+ : PROVIDER_ENV_NAMES[provider];
58
+ if (envNames) {
59
+ for (const name of envNames) {
60
+ const value = env[name];
61
+ if (value !== undefined && value.length > 0) {
62
+ return {
63
+ key: value,
64
+ source: 'env',
65
+ provider,
66
+ envName: name,
67
+ };
68
+ }
69
+ }
70
+ }
71
+ // Step 2: credentials file at operator's 'global' scope. If the
72
+ // operator set a key here, every user uses it (operator pays).
73
+ const globalHit = await fileLookupAtScope(BYOK_GLOBAL_APP_SCOPE, provider);
74
+ if (globalHit)
75
+ return globalHit;
76
+ // Step 3: credentials file at the end-user's identity scope.
77
+ // Only consulted when the operator opted out of paying (no
78
+ // global key). The /settings UI under --multi-tenant writes
79
+ // here.
80
+ if (resolveOpts?.userScope &&
81
+ resolveOpts.userScope !== BYOK_GLOBAL_APP_SCOPE) {
82
+ const userHit = await fileLookupAtScope(resolveOpts.userScope, provider);
83
+ if (userHit)
84
+ return userHit;
85
+ }
86
+ return null;
87
+ },
88
+ };
89
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}