aidevops 3.31.74 → 3.31.76

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 CHANGED
@@ -60,7 +60,7 @@ The result: an AI operations platform that manages projects across every busines
60
60
  [![Copyright](https://img.shields.io/badge/Copyright-Marcus%20Quinn%202025--2026-blue.svg)](https://github.com/marcusquinn)
61
61
 
62
62
  <!-- Release & Version Info -->
63
- [![Version](https://img.shields.io/badge/Version-3.31.74-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
63
+ [![Version](https://img.shields.io/badge/Version-3.31.76-blue.svg)](https://github.com/marcusquinn/aidevops/releases)
64
64
  [![npm version](https://img.shields.io/npm/v/aidevops)](https://www.npmjs.com/package/aidevops)
65
65
  [![Homebrew](https://img.shields.io/badge/homebrew-marcusquinn%2Ftap-orange)](https://github.com/marcusquinn/homebrew-tap)
66
66
  [![GitHub repository](https://img.shields.io/badge/github-repository-181717.svg?logo=github)](https://github.com/marcusquinn/aidevops)
package/VERSION CHANGED
@@ -1 +1 @@
1
- 3.31.74
1
+ 3.31.76
package/aidevops.sh CHANGED
@@ -5,7 +5,7 @@
5
5
  # AI DevOps Framework CLI
6
6
  # Usage: aidevops <command> [options]
7
7
  #
8
- # Version: 3.31.74
8
+ # Version: 3.31.76
9
9
 
10
10
  set -euo pipefail
11
11
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aidevops",
3
- "version": "3.31.74",
3
+ "version": "3.31.76",
4
4
  "description": "AI DevOps Framework - AI-assisted development workflows, code quality, and deployment automation",
5
5
  "type": "module",
6
6
  "workspaces": [
@@ -191,7 +191,7 @@ export function readVaultStatus(
191
191
  return envelope;
192
192
  }
193
193
 
194
- const AI_PROVIDER_IDS: GuiAiProviderId[] = ["anthropic", "openai", "cursor", "google"];
194
+ const AI_PROVIDER_IDS: GuiAiProviderId[] = ["anthropic", "openai", "cursor", "google", "zai"];
195
195
  const SETUP_TARGET_DEFINITIONS: Array<Pick<GuiSetupTargetSummary, "label" | "path_ref" | "purpose">> = [
196
196
  {
197
197
  label: "Deployed agents",
@@ -446,7 +446,7 @@ export interface GuiConversationThread {
446
446
  read_states: GuiConversationReadState[];
447
447
  }
448
448
 
449
- export type GuiAiProviderId = "anthropic" | "openai" | "cursor" | "google";
449
+ export type GuiAiProviderId = "anthropic" | "openai" | "cursor" | "google" | "zai";
450
450
 
451
451
  export interface GuiOAuthPoolAccountSummary {
452
452
  email_ref: string;
@@ -254,6 +254,17 @@ export const statusFixture: GuiStatusData = {
254
254
  pending_token: false,
255
255
  accounts: [],
256
256
  },
257
+ {
258
+ provider: "zai",
259
+ configured: false,
260
+ total: 0,
261
+ available: 0,
262
+ active_or_idle: 0,
263
+ rate_limited: 0,
264
+ auth_errors: 0,
265
+ pending_token: false,
266
+ accounts: [],
267
+ },
257
268
  ],
258
269
  },
259
270
  setup_targets: [
@@ -351,15 +362,26 @@ export const statusFixture: GuiStatusData = {
351
362
  preview_policy: "hidden_while_locked",
352
363
  actions_policy: "disabled_while_locked",
353
364
  },
365
+ {
366
+ id: "security-routing",
367
+ label: "Security routing metadata",
368
+ data_class: "config metadata",
369
+ labels: ["internal", "provider-allowed"],
370
+ surface_ids: ["security"],
371
+ encrypted: true,
372
+ state: "not_configured",
373
+ preview_policy: "hidden_while_locked",
374
+ actions_policy: "disabled_while_locked",
375
+ },
354
376
  {
355
377
  id: "provider-routing",
356
378
  label: "Provider routing metadata",
357
379
  data_class: "config metadata",
358
380
  labels: ["internal", "provider-allowed"],
359
- surface_ids: ["security", "aiProviders"],
381
+ surface_ids: ["aiProviders"],
360
382
  encrypted: true,
361
383
  state: "not_configured",
362
- preview_policy: "hidden_while_locked",
384
+ preview_policy: "metadata_only",
363
385
  actions_policy: "disabled_while_locked",
364
386
  },
365
387
  {
@@ -0,0 +1,297 @@
1
+ import { useState } from "react";
2
+ import type { IconType } from "react-icons";
3
+ import { FiAlertTriangle, FiCheckCircle, FiClock, FiCommand, FiGlobe, FiInfo, FiKey, FiPlusCircle, FiRefreshCw, FiRepeat, FiShield, FiTerminal, FiThumbsUp, FiTool } from "react-icons/fi";
4
+ import type { GuiOAuthProviderSummary, GuiStatusData } from "../../gui-shared/src";
5
+ import { text } from "./app-model";
6
+
7
+ type AiProviderGroupId = "recommended-oauth" | "oauth-pool";
8
+
9
+ interface AiProviderCatalogEntry {
10
+ id: GuiOAuthProviderSummary["provider"];
11
+ displayName: string;
12
+ group: AiProviderGroupId;
13
+ authKind: string;
14
+ description: string;
15
+ connectionHint: string;
16
+ recommended: boolean;
17
+ recommendation?: string;
18
+ opencodeProviders: string[];
19
+ capabilities: string[];
20
+ Icon: IconType;
21
+ }
22
+
23
+ const AI_PROVIDER_GROUPS: Array<{ detail: string; id: AiProviderGroupId; label: string }> = [
24
+ { id: "recommended-oauth", label: "Recommended OAuth pools", detail: "First-class providers to connect for aidevops model rotation and worker resilience." },
25
+ { id: "oauth-pool", label: "OAuth pool compatible", detail: "Additional OpenCode providers that can be represented by the local aidevops OAuth pool." },
26
+ ];
27
+
28
+ const AI_PROVIDER_CATALOG: AiProviderCatalogEntry[] = [
29
+ {
30
+ id: "openai",
31
+ displayName: "OpenAI",
32
+ group: "recommended-oauth",
33
+ authKind: "OAuth pool",
34
+ description: "Primary coding and reasoning models through OpenCode's OpenAI provider and the aidevops OAuth pool.",
35
+ connectionHint: "Use OpenCode auth login, choose OpenAI Pool, then add each account email separately.",
36
+ recommended: true,
37
+ recommendation: "Recommended for strong coding coverage and current OAuth rotation support.",
38
+ opencodeProviders: ["openai", "openai-pool", "openrouter/openai"],
39
+ capabilities: ["coding", "reasoning", "pool rotation"],
40
+ Icon: FiCommand,
41
+ },
42
+ {
43
+ id: "cursor",
44
+ displayName: "Cursor",
45
+ group: "recommended-oauth",
46
+ authKind: "OAuth proxy",
47
+ description: "Cursor account pool routed through the aidevops Cursor proxy for OpenCode-compatible model access.",
48
+ connectionHint: "Use OpenCode auth login, choose Cursor Pool, then keep every account as a separate pool entry.",
49
+ recommended: true,
50
+ recommendation: "Recommended when Cursor subscriptions provide useful fallback capacity.",
51
+ opencodeProviders: ["cursor", "cursor-pool"],
52
+ capabilities: ["subscription reuse", "proxy", "fallback capacity"],
53
+ Icon: FiTerminal,
54
+ },
55
+ {
56
+ id: "zai",
57
+ displayName: "Z.ai",
58
+ group: "recommended-oauth",
59
+ authKind: "OAuth pool",
60
+ description: "GLM/Z.ai provider family surfaced by OpenCode as zai, z-ai, and zhipuai model prefixes.",
61
+ connectionHint: "Track Z.ai as a pool target now; audited OAuth connection routes should be added before enabling writes.",
62
+ recommended: true,
63
+ recommendation: "Recommended for GLM coding and low-friction non-US provider diversity.",
64
+ opencodeProviders: ["zai", "zai-coding-plan", "zhipuai", "openrouter/z-ai"],
65
+ capabilities: ["GLM models", "provider diversity", "planned OAuth"],
66
+ Icon: FiTool,
67
+ },
68
+ {
69
+ id: "anthropic",
70
+ displayName: "Anthropic",
71
+ group: "oauth-pool",
72
+ authKind: "OAuth pool",
73
+ description: "Claude provider pool for compatible Anthropic and Claude Code sessions.",
74
+ connectionHint: "Use OpenCode auth login, choose Anthropic Pool, then rotate accounts with the local OAuth pool helper.",
75
+ recommended: false,
76
+ opencodeProviders: ["anthropic", "openrouter/anthropic", "poe/anthropic"],
77
+ capabilities: ["Claude models", "OAuth", "account rotation"],
78
+ Icon: FiShield,
79
+ },
80
+ {
81
+ id: "google",
82
+ displayName: "Google Gemini",
83
+ group: "oauth-pool",
84
+ authKind: "OAuth pool",
85
+ description: "Gemini and Vertex-compatible OAuth accounts represented as metadata-only pool entries.",
86
+ connectionHint: "Use OpenCode auth login, choose Google Pool, and keep Workspace or AI subscription accounts separate.",
87
+ recommended: false,
88
+ opencodeProviders: ["google", "gemini", "openrouter/google", "vercel/google"],
89
+ capabilities: ["Gemini", "Vertex", "multimodal"],
90
+ Icon: FiGlobe,
91
+ },
92
+ ];
93
+
94
+ export function AiProvidersSurface({ status }: { status: GuiStatusData }) {
95
+ const [activeGroup, setActiveGroup] = useState<AiProviderGroupId | "all">("all");
96
+ const [query, setQuery] = useState("");
97
+ const providersById = new Map(status.oauth_pool.providers.map((provider) => [provider.provider, provider]));
98
+ const catalogCards = AI_PROVIDER_CATALOG.map((entry) => ({ entry, provider: providersById.get(entry.id) ?? emptyProviderSummary(entry.id) }));
99
+ const normalizedQuery = query.trim().toLowerCase();
100
+ const visibleCards = catalogCards.filter(({ entry }) => {
101
+ const matchesGroup = activeGroup === "all" || entry.group === activeGroup;
102
+ const searchable = [entry.displayName, entry.id, entry.authKind, entry.description, entry.connectionHint, ...entry.opencodeProviders, ...entry.capabilities].join(" ").toLowerCase();
103
+
104
+ return matchesGroup && (normalizedQuery.length === 0 || searchable.includes(normalizedQuery));
105
+ });
106
+ const totalAccounts = catalogCards.reduce((sum, card) => sum + card.provider.total, 0);
107
+ const availableAccounts = catalogCards.reduce((sum, card) => sum + card.provider.available, 0);
108
+ const recommendedConnected = catalogCards.filter((card) => card.entry.recommended && card.provider.configured).length;
109
+ const needsAttention = catalogCards.filter((card) => card.provider.auth_errors > 0 || card.provider.rate_limited > 0 || card.provider.pending_token).length;
110
+ const summaryCards = [
111
+ { label: "accounts", value: String(totalAccounts), detail: "metadata-only pool entries" },
112
+ { label: "available", value: String(availableAccounts), detail: "ready after cooldown checks" },
113
+ { label: "recommended", value: `${recommendedConnected}/3`, detail: "OpenAI, Cursor, and Z.ai" },
114
+ { label: "attention", value: String(needsAttention), detail: "auth errors, rate limits, pending tokens" },
115
+ ];
116
+
117
+ return (
118
+ <section className="surface-page ai-providers-surface" aria-label={text.aiProviders}>
119
+ <div className="hero-panel ai-provider-hero">
120
+ <div className="section-heading split-heading">
121
+ <div>
122
+ <p className="eyebrow">{status.oauth_pool.path_ref}</p>
123
+ <h2>{text.aiProviders}</h2>
124
+ <p>{text.aiProvidersIntro} Manage provider connections as OAuth pool metadata, with one card per OpenCode provider family and one row per account.</p>
125
+ </div>
126
+ <span className="count-pill">{status.oauth_pool.value_policy}</span>
127
+ </div>
128
+ <div className="ai-provider-summary-grid">
129
+ {summaryCards.map((card) => (
130
+ <article className="ai-provider-summary-card" key={card.label}>
131
+ <span>{card.label}</span>
132
+ <strong>{card.value}</strong>
133
+ <small>{card.detail}</small>
134
+ </article>
135
+ ))}
136
+ </div>
137
+ </div>
138
+ <div className="notice compact-notice" role="note">
139
+ Data-leaves-device warning: provider accounts can process prompts outside this machine. Use `data_classification` and `runtime_policy` metadata to require Local AI or explicit provider approval for confidential work.
140
+ </div>
141
+ <section className="panel ai-provider-catalog-panel" aria-label="AI provider catalog">
142
+ <div className="provider-toolbar">
143
+ <label className="provider-search">
144
+ <FiInfo aria-hidden="true" />
145
+ <span className="sr-only">Search AI providers</span>
146
+ <input aria-label="Search AI providers" onChange={(event) => setQuery(event.target.value)} placeholder="Search providers, model prefixes, or capabilities" type="search" value={query} />
147
+ </label>
148
+ <div aria-label="Provider group filters" className="provider-filter-chips" role="toolbar">
149
+ <button className={activeGroup === "all" ? "active" : ""} onClick={() => setActiveGroup("all")} type="button">All <span>{catalogCards.length}</span></button>
150
+ {AI_PROVIDER_GROUPS.map((group) => (
151
+ <button className={activeGroup === group.id ? "active" : ""} key={group.id} onClick={() => setActiveGroup(group.id)} type="button">
152
+ {group.label} <span>{catalogCards.filter((card) => card.entry.group === group.id).length}</span>
153
+ </button>
154
+ ))}
155
+ </div>
156
+ </div>
157
+ {visibleCards.length === 0 ? (
158
+ <p className="empty-state">No providers match this filter.</p>
159
+ ) : (
160
+ AI_PROVIDER_GROUPS.map((group) => {
161
+ const groupCards = visibleCards.filter((card) => card.entry.group === group.id);
162
+ if (groupCards.length === 0) {
163
+ return null;
164
+ }
165
+
166
+ return (
167
+ <section className="provider-section" key={group.id} aria-label={group.label}>
168
+ <div className="section-heading split-heading provider-section-heading">
169
+ <div>
170
+ <p className="eyebrow">{groupCards.length} provider{groupCards.length === 1 ? "" : "s"}</p>
171
+ <h3>{group.label}</h3>
172
+ <p>{group.detail}</p>
173
+ </div>
174
+ </div>
175
+ <div className="provider-grid">
176
+ {groupCards.map((card) => <ProviderCard entry={card.entry} key={card.entry.id} provider={card.provider} />)}
177
+ </div>
178
+ </section>
179
+ );
180
+ })
181
+ )}
182
+ </section>
183
+ </section>
184
+ );
185
+ }
186
+
187
+ function ProviderCard({ entry, provider }: { entry: AiProviderCatalogEntry; provider: GuiOAuthProviderSummary }) {
188
+ const Icon = entry.Icon;
189
+ const connectLabel = provider.configured ? "Add another account" : "Connect account";
190
+ const connectionTitle = `${entry.connectionHint} Browser write routes are read-only until audited provider actions are implemented.`;
191
+
192
+ return (
193
+ <article className={entry.recommended ? "provider-card provider-card-recommended" : "provider-card"}>
194
+ <header className="provider-card-header">
195
+ <div className="provider-identity">
196
+ <span className={`provider-avatar provider-avatar-${entry.id}`} aria-hidden="true"><Icon /></span>
197
+ <div>
198
+ <p className="eyebrow">{entry.authKind}</p>
199
+ <div className="provider-title-row">
200
+ <h3>{entry.displayName}</h3>
201
+ {entry.recommended ? <span className="recommendation-badge" title={entry.recommendation}><FiThumbsUp aria-hidden="true" />Recommended</span> : null}
202
+ </div>
203
+ <p>{entry.description}</p>
204
+ </div>
205
+ </div>
206
+ <div className="provider-count-stack">
207
+ <strong>{provider.available}/{provider.total}</strong>
208
+ <ProviderStatusBadge provider={provider} />
209
+ </div>
210
+ </header>
211
+ <ul aria-label={`${entry.displayName} OpenCode provider prefixes`} className="provider-chip-row">
212
+ {entry.opencodeProviders.map((prefix) => <li className="provider-chip" key={prefix}>{prefix}</li>)}
213
+ </ul>
214
+ <p className="provider-connection-hint">{entry.connectionHint}</p>
215
+ <div className="repo-card-meta provider-metrics">
216
+ <ProviderDetail label="active/idle" value={String(provider.active_or_idle)} />
217
+ <ProviderDetail label="rate limited" value={String(provider.rate_limited)} />
218
+ <ProviderDetail label="auth errors" value={String(provider.auth_errors)} />
219
+ <ProviderDetail label="pending token" value={provider.pending_token ? "yes" : "no"} />
220
+ </div>
221
+ <ul aria-label={`${entry.displayName} capabilities`} className="provider-chip-row provider-capability-row">
222
+ {entry.capabilities.map((capability) => <li className="provider-chip" key={capability}>{capability}</li>)}
223
+ </ul>
224
+ <fieldset className="provider-actions">
225
+ <legend className="sr-only">{entry.displayName} planned management controls</legend>
226
+ <button disabled title={connectionTitle} type="button"><FiPlusCircle aria-hidden="true" />{connectLabel}</button>
227
+ <button disabled title="Read-only preview: rotation uses oauth-pool-helper.sh rotate after audited write routes land." type="button"><FiRepeat aria-hidden="true" />Rotate</button>
228
+ <button disabled title="Read-only preview: health checks use oauth-pool-helper.sh check after audited write routes land." type="button"><FiRefreshCw aria-hidden="true" />Check</button>
229
+ <button disabled title="Read-only preview: cooldown resets use oauth-pool-helper.sh reset-cooldowns after audited write routes land." type="button"><FiClock aria-hidden="true" />Cooldowns</button>
230
+ </fieldset>
231
+ {provider.accounts.length === 0 ? (
232
+ <p className="empty-state compact-empty">No accounts in this pool yet. Multiple accounts are supported by adding a separate OAuth entry per email.</p>
233
+ ) : (
234
+ <ul className="provider-account-list">
235
+ {provider.accounts.map((account) => (
236
+ <li key={`${provider.provider}:${account.email_ref}`}>
237
+ <strong>{account.email_ref}</strong>
238
+ <span className="provider-account-status">{account.status}</span>
239
+ <small>priority {account.priority ?? "default"}</small>
240
+ <small>last used {account.last_used}</small>
241
+ <small>expires {account.expires_at}</small>
242
+ {account.cooldown_until ? <small>cooldown {account.cooldown_until}</small> : null}
243
+ <button disabled title="Read-only preview: account management requires audited provider write routes." type="button"><FiKey aria-hidden="true" />Manage</button>
244
+ </li>
245
+ ))}
246
+ </ul>
247
+ )}
248
+ </article>
249
+ );
250
+ }
251
+
252
+ function ProviderStatusBadge({ provider }: { provider: GuiOAuthProviderSummary }) {
253
+ const status = providerStatus(provider);
254
+ const Icon = status.Icon;
255
+
256
+ return <span className={`status-pill provider-health ${status.className}`}><Icon aria-hidden="true" />{status.label}</span>;
257
+ }
258
+
259
+ function providerStatus(provider: GuiOAuthProviderSummary): { Icon: IconType; className: string; label: string } {
260
+ if (provider.auth_errors > 0) {
261
+ return { Icon: FiAlertTriangle, className: "error", label: "auth attention" };
262
+ }
263
+ if (provider.rate_limited > 0) {
264
+ return { Icon: FiClock, className: "warn", label: "rate limited" };
265
+ }
266
+ if (provider.pending_token) {
267
+ return { Icon: FiKey, className: "warn", label: "pending token" };
268
+ }
269
+ if (provider.configured) {
270
+ return { Icon: FiCheckCircle, className: "ready", label: "configured" };
271
+ }
272
+
273
+ return { Icon: FiInfo, className: "neutral", label: "not connected" };
274
+ }
275
+
276
+ function emptyProviderSummary(provider: GuiOAuthProviderSummary["provider"]): GuiOAuthProviderSummary {
277
+ return {
278
+ provider,
279
+ configured: false,
280
+ total: 0,
281
+ available: 0,
282
+ active_or_idle: 0,
283
+ rate_limited: 0,
284
+ auth_errors: 0,
285
+ pending_token: false,
286
+ accounts: [],
287
+ };
288
+ }
289
+
290
+ function ProviderDetail({ label, value }: { label: string; value: string }) {
291
+ return (
292
+ <span>
293
+ <small>{label}</small>
294
+ <strong>{value}</strong>
295
+ </span>
296
+ );
297
+ }
@@ -37,7 +37,7 @@ import {
37
37
  import type { ContrastPreference, ConversationMode, FontPreference, FontSizePreference, ShellMode, SidebarMode, SurfaceIconName, SurfaceId, SurfaceNavGroup, SurfaceNavItem, ThemePreference } from "./app-model";
38
38
  import { dashboardNavItem, navGroups, sidebarModeForSurface, surfaceRecordCounts, text } from "./app-model";
39
39
  import { SidebarFooter, wrappedOptionIndex } from "./AppearanceControls";
40
- import { VaultPadlock, vaultCollectionForSurface } from "./VaultBadges";
40
+ import { VaultPadlock, vaultCollectionForSurface, vaultCollectionTooltip } from "./VaultBadges";
41
41
 
42
42
  function AidevopsPromptIcon() {
43
43
  return (
@@ -338,7 +338,7 @@ function SidebarItem({ activeSurface, item, recordCount, setActiveSurface, showC
338
338
  }) {
339
339
  const isActive = activeSurface === item.id;
340
340
  const vaultCollection = vaultCollectionForSurface(status.vault, item.id);
341
- const vaultTooltip = vaultCollection ? ` ${text.vaultTooltip}` : "";
341
+ const vaultTooltip = vaultCollection ? ` ${vaultCollectionTooltip(vaultCollection)}` : "";
342
342
  const tooltip = `${item.label}: ${item.description}${vaultTooltip}`;
343
343
  const shouldShowCount = showCount && recordCount !== undefined && recordCount > 0;
344
344
 
@@ -1,11 +1,13 @@
1
1
  /* jshint esversion: 11 */
2
2
  import { useState } from "react";
3
- import type { GuiAiAppSummary, GuiLocalRepoSetupSummary, GuiOAuthProviderSummary, GuiSetupTargetSummary, GuiStatusData, GuiVaultCollectionSummary, GuiVaultStatusData } from "../../gui-shared/src";
3
+ import type { GuiAiAppSummary, GuiLocalRepoSetupSummary, GuiSetupTargetSummary, GuiStatusData, GuiVaultCollectionSummary, GuiVaultStatusData } from "../../gui-shared/src";
4
4
  import { plannedHomes, text } from "./app-model";
5
5
  import { FileExplorerSurface } from "./FileExplorerSurface";
6
6
  import { PathActions } from "./PathActions";
7
7
  import { VaultPadlock } from "./VaultBadges";
8
8
 
9
+ export { AiProvidersSurface } from "./AiProvidersSurface";
10
+
9
11
  export function OverviewSurface({ status }: { status: GuiStatusData }) {
10
12
  const metrics = [
11
13
  { label: text.setup, value: status.update.restart_required ? "restart" : "current", detail: status.update.installed_version },
@@ -150,9 +152,9 @@ export function VaultSurface({ status }: { status: GuiStatusData }) {
150
152
  </div>
151
153
  {vaultCollection ? <VaultPadlock collection={vaultCollection} vault={vault} /> : null}
152
154
  </div>
153
- <div className="vault-readiness-strip" aria-label="Vault readiness">
154
- {readiness.map((item) => <span key={item.label}><strong>{item.label}</strong>{item.value}</span>)}
155
- </div>
155
+ <ul aria-label="Vault readiness" className="vault-readiness-strip">
156
+ {readiness.map((item) => <li key={item.label}><strong>{item.label}</strong>{item.value}</li>)}
157
+ </ul>
156
158
  </div>
157
159
  {vault.locked ? (
158
160
  <div className="notice compact-notice" role="note">
@@ -445,66 +447,6 @@ function AiAppCard({ app }: { app: GuiAiAppSummary }) {
445
447
  );
446
448
  }
447
449
 
448
- export function AiProvidersSurface({ status }: { status: GuiStatusData }) {
449
- return (
450
- <section className="panel ai-providers-panel" aria-label={text.aiProviders}>
451
- <div className="section-heading split-heading">
452
- <div>
453
- <p className="eyebrow">{status.oauth_pool.path_ref}</p>
454
- <h2>{text.aiProviders}</h2>
455
- <p>{text.aiProvidersIntro}</p>
456
- </div>
457
- <span className="count-pill">{status.oauth_pool.value_policy}</span>
458
- </div>
459
- <div className="notice compact-notice" role="note">
460
- Data-leaves-device warning: provider accounts can process prompts outside this machine. Use `data_classification` and `runtime_policy` metadata to require Local AI or explicit provider approval for confidential work.
461
- </div>
462
- <div className="provider-grid">
463
- {status.oauth_pool.providers.map((provider) => <ProviderCard key={provider.provider} provider={provider} />)}
464
- </div>
465
- </section>
466
- );
467
- }
468
-
469
- function ProviderCard({ provider }: { provider: GuiOAuthProviderSummary }) {
470
- return (
471
- <article className="provider-card">
472
- <header>
473
- <div>
474
- <p className="eyebrow">{provider.configured ? "configured" : "not configured"}</p>
475
- <h3>{provider.provider}</h3>
476
- </div>
477
- <strong>{provider.available}/{provider.total}</strong>
478
- </header>
479
- <div className="repo-card-meta provider-metrics">
480
- <Detail label="active/idle" value={String(provider.active_or_idle)} />
481
- <Detail label="rate limited" value={String(provider.rate_limited)} />
482
- <Detail label="auth errors" value={String(provider.auth_errors)} />
483
- <Detail label="pending token" value={provider.pending_token ? "yes" : "no"} />
484
- </div>
485
- <fieldset className="provider-actions" aria-label={`${provider.provider} planned management controls`}>
486
- {['Add account', 'Rotate', 'Check', 'Reset cooldowns'].map((action) => <button disabled key={action} type="button">{action}</button>)}
487
- </fieldset>
488
- {provider.accounts.length === 0 ? (
489
- <p className="empty-state compact-empty">No accounts in this pool.</p>
490
- ) : (
491
- <ul className="provider-account-list">
492
- {provider.accounts.map((account) => (
493
- <li key={`${provider.provider}:${account.email_ref}`}>
494
- <strong>{account.email_ref}</strong>
495
- <span>{account.status}</span>
496
- <small>priority {account.priority ?? "default"}</small>
497
- <small>expires {account.expires_at}</small>
498
- {account.cooldown_until ? <small>cooldown {account.cooldown_until}</small> : null}
499
- <button disabled type="button">Manage</button>
500
- </li>
501
- ))}
502
- </ul>
503
- )}
504
- </article>
505
- );
506
- }
507
-
508
450
  function Detail({ label, value }: { label: string; value: string }) {
509
451
  return (
510
452
  <span>
@@ -12,7 +12,15 @@ export function vaultCollectionForSurface(
12
12
 
13
13
  export function isVaultSurfaceLocked(vault: GuiVaultStatusData, surface: SurfaceId): boolean {
14
14
  const collection = vaultCollectionForSurface(vault, surface);
15
- return surface !== "vault" && collection?.encrypted === true && !vault.unlocked;
15
+ return surface !== "vault" && collection?.encrypted === true && collection.preview_policy === "hidden_while_locked" && !vault.unlocked;
16
+ }
17
+
18
+ export function vaultCollectionTooltip(collection: GuiVaultCollectionSummary): string {
19
+ if (collection.preview_policy === "metadata_only") {
20
+ return text.vaultMetadataPreview;
21
+ }
22
+
23
+ return text.vaultTooltip;
16
24
  }
17
25
 
18
26
  export function VaultPadlock({ collection, compact = false, vault }: {
@@ -23,11 +31,10 @@ export function VaultPadlock({ collection, compact = false, vault }: {
23
31
  const locked = collection.state !== "unlocked" || !vault.unlocked;
24
32
  const stateLabel = locked ? "Locked" : "Unlocked";
25
33
  const Icon = locked ? FiLock : FiUnlock;
26
- const tooltip = `${stateLabel}: ${text.vaultTooltip}`;
34
+ const tooltip = `${stateLabel}: ${vaultCollectionTooltip(collection)}`;
27
35
 
28
36
  return (
29
37
  <span
30
- aria-label={`${stateLabel} by aidevops Vault: ${collection.label}`}
31
38
  className={compact ? "vault-padlock compact" : "vault-padlock"}
32
39
  data-vault-state={locked ? "locked" : "unlocked"}
33
40
  title={tooltip}
@@ -316,6 +316,7 @@ export const text = {
316
316
  vaultSetup: "First-use Setup",
317
317
  vaultStatus: "Status",
318
318
  vaultSync: "Sync",
319
+ vaultMetadataPreview: "Encrypted by aidevops Vault; metadata previews stay visible while locked, and protected previews plus write actions require a local unlock.",
319
320
  vaultTooltip: "Encrypted by aidevops Vault; contents visible only when unlocked through app or authorised vault commands.",
320
321
  vaultUnlockCta: "Unlock Vault",
321
322
  vaultUnlockHint: "Run `aidevops vault unlock` in a local terminal and enter the passphrase only into the hidden prompt.",
@@ -1930,9 +1930,12 @@ code {
1930
1930
  display: flex;
1931
1931
  flex-wrap: wrap;
1932
1932
  gap: 8px;
1933
+ list-style: none;
1934
+ margin: 0;
1935
+ padding: 0;
1933
1936
  }
1934
1937
 
1935
- .vault-readiness-strip span {
1938
+ .vault-readiness-strip li {
1936
1939
  background: var(--surface-elevated);
1937
1940
  border: 1px solid var(--border);
1938
1941
  border-radius: 999px;
@@ -3561,6 +3564,7 @@ button.managed-toggle:focus-visible {
3561
3564
 
3562
3565
  .repo-card-grid,
3563
3566
  .provider-grid,
3567
+ .ai-provider-summary-grid,
3564
3568
  .setup-card-grid,
3565
3569
  .ai-app-grid,
3566
3570
  .vault-card-grid,
@@ -3572,6 +3576,7 @@ button.managed-toggle:focus-visible {
3572
3576
 
3573
3577
  .repo-setup-card,
3574
3578
  .provider-card,
3579
+ .ai-provider-summary-card,
3575
3580
  .setup-target-card,
3576
3581
  .ai-app-card,
3577
3582
  .vault-feature-card,
@@ -3587,6 +3592,7 @@ button.managed-toggle:focus-visible {
3587
3592
 
3588
3593
  .repo-setup-card header,
3589
3594
  .provider-card header,
3595
+ .provider-card-header,
3590
3596
  .setup-target-card header,
3591
3597
  .ai-app-card header,
3592
3598
  .repo-card-footer {
@@ -4065,12 +4071,211 @@ button.managed-toggle:focus-visible {
4065
4071
  transform: translateX(18px);
4066
4072
  }
4067
4073
 
4068
- .provider-card > header > strong {
4074
+ .ai-provider-summary-grid {
4075
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
4076
+ }
4077
+
4078
+ .ai-provider-summary-card span {
4079
+ color: var(--text-muted);
4080
+ font-size: 0.74rem;
4081
+ font-weight: 900;
4082
+ text-transform: uppercase;
4083
+ }
4084
+
4085
+ .ai-provider-summary-card strong {
4086
+ color: var(--accent);
4087
+ display: block;
4088
+ font-size: 1.55rem;
4089
+ letter-spacing: -0.05em;
4090
+ }
4091
+
4092
+ .ai-provider-summary-card small,
4093
+ .provider-card p,
4094
+ .provider-connection-hint {
4095
+ color: var(--text-secondary);
4096
+ }
4097
+
4098
+ .ai-provider-catalog-panel,
4099
+ .provider-section {
4100
+ display: grid;
4101
+ gap: 16px;
4102
+ }
4103
+
4104
+ .provider-toolbar {
4105
+ align-items: center;
4106
+ display: grid;
4107
+ gap: 10px;
4108
+ grid-template-columns: minmax(260px, 1fr) auto;
4109
+ }
4110
+
4111
+ .provider-search {
4112
+ align-items: center;
4113
+ background: var(--surface-elevated);
4114
+ border: 1px solid var(--border);
4115
+ border-radius: 999px;
4116
+ color: var(--text-muted);
4117
+ display: flex;
4118
+ gap: 8px;
4119
+ min-width: 0;
4120
+ padding: 8px 12px;
4121
+ }
4122
+
4123
+ .provider-search input {
4124
+ background: transparent;
4125
+ border: 0;
4126
+ color: var(--text-primary);
4127
+ min-width: 0;
4128
+ outline: 0;
4129
+ width: 100%;
4130
+ }
4131
+
4132
+ .provider-search:focus-within {
4133
+ border-color: var(--border-accent);
4134
+ box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 16%, transparent);
4135
+ }
4136
+
4137
+ .provider-filter-chips,
4138
+ .provider-chip-row {
4139
+ display: flex;
4140
+ flex-wrap: wrap;
4141
+ gap: 7px;
4142
+ list-style: none;
4143
+ margin: 0;
4144
+ padding: 0;
4145
+ }
4146
+
4147
+ .provider-filter-chips button,
4148
+ .provider-chip,
4149
+ .recommendation-badge,
4150
+ .provider-health {
4151
+ align-items: center;
4152
+ border-radius: 999px;
4153
+ display: inline-flex;
4154
+ gap: 5px;
4155
+ white-space: nowrap;
4156
+ }
4157
+
4158
+ .provider-filter-chips button,
4159
+ .provider-chip {
4160
+ background: var(--surface-elevated);
4161
+ border: 1px solid var(--border);
4162
+ color: var(--text-secondary);
4163
+ font-size: 0.76rem;
4164
+ font-weight: 800;
4165
+ padding: 6px 9px;
4166
+ }
4167
+
4168
+ .provider-filter-chips button.active {
4169
+ border-color: var(--border-accent);
4170
+ color: var(--accent);
4171
+ }
4172
+
4173
+ .provider-filter-chips span {
4174
+ color: var(--text-muted);
4175
+ }
4176
+
4177
+ .provider-section-heading h3 {
4178
+ margin: 0 0 4px;
4179
+ }
4180
+
4181
+ .provider-card-recommended {
4182
+ border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
4183
+ }
4184
+
4185
+ .provider-identity {
4186
+ align-items: flex-start;
4187
+ display: flex;
4188
+ gap: 12px;
4189
+ min-width: 0;
4190
+ }
4191
+
4192
+ .provider-avatar {
4193
+ align-items: center;
4194
+ background: color-mix(in srgb, var(--accent) 14%, transparent);
4195
+ border: 1px solid color-mix(in srgb, var(--accent) 36%, transparent);
4196
+ border-radius: 14px;
4197
+ color: var(--accent);
4198
+ display: inline-flex;
4199
+ flex: 0 0 auto;
4200
+ font-size: 1.2rem;
4201
+ height: 42px;
4202
+ justify-content: center;
4203
+ width: 42px;
4204
+ }
4205
+
4206
+ .provider-avatar-openai,
4207
+ .provider-avatar-zai {
4208
+ background: color-mix(in srgb, var(--success-text) 12%, transparent);
4209
+ border-color: color-mix(in srgb, var(--success-text) 34%, transparent);
4210
+ color: var(--success-text);
4211
+ }
4212
+
4213
+ .provider-avatar-cursor {
4214
+ background: color-mix(in srgb, var(--text-primary) 10%, transparent);
4215
+ color: var(--text-primary);
4216
+ }
4217
+
4218
+ .provider-title-row {
4219
+ align-items: center;
4220
+ display: flex;
4221
+ flex-wrap: wrap;
4222
+ gap: 8px;
4223
+ }
4224
+
4225
+ .provider-title-row h3 {
4226
+ margin: 0;
4227
+ }
4228
+
4229
+ .recommendation-badge {
4230
+ background: color-mix(in srgb, var(--success-text) 14%, transparent);
4231
+ border: 1px solid color-mix(in srgb, var(--success-text) 38%, transparent);
4232
+ color: var(--success-text);
4233
+ font-size: 0.7rem;
4234
+ font-weight: 900;
4235
+ padding: 4px 7px;
4236
+ text-transform: uppercase;
4237
+ }
4238
+
4239
+ .provider-count-stack {
4240
+ align-items: flex-end;
4241
+ display: grid;
4242
+ gap: 6px;
4243
+ justify-items: end;
4244
+ }
4245
+
4246
+ .provider-count-stack > strong {
4069
4247
  color: var(--accent);
4070
4248
  font-size: 1.8rem;
4071
4249
  letter-spacing: -0.06em;
4072
4250
  }
4073
4251
 
4252
+ .provider-health.ready {
4253
+ background: color-mix(in srgb, var(--success-text) 14%, transparent);
4254
+ border-color: color-mix(in srgb, var(--success-text) 38%, transparent);
4255
+ color: var(--success-text);
4256
+ }
4257
+
4258
+ .provider-health.error {
4259
+ background: color-mix(in srgb, var(--notification-error-fg) 14%, transparent);
4260
+ border-color: color-mix(in srgb, var(--notification-error-fg) 38%, transparent);
4261
+ color: var(--notification-error-fg);
4262
+ }
4263
+
4264
+ .provider-health.neutral {
4265
+ background: var(--surface-elevated);
4266
+ border-color: var(--border);
4267
+ color: var(--text-muted);
4268
+ }
4269
+
4270
+ .provider-connection-hint {
4271
+ background: var(--surface-elevated);
4272
+ border: 1px solid var(--border);
4273
+ border-radius: 12px;
4274
+ font-size: 0.84rem;
4275
+ margin: 0;
4276
+ padding: 10px;
4277
+ }
4278
+
4074
4279
  .provider-actions {
4075
4280
  border: 0;
4076
4281
  display: flex;
@@ -4082,10 +4287,13 @@ button.managed-toggle:focus-visible {
4082
4287
 
4083
4288
  .provider-actions button,
4084
4289
  .provider-account-list button {
4290
+ align-items: center;
4085
4291
  background: var(--surface-elevated);
4086
4292
  border: 1px solid var(--border);
4087
4293
  border-radius: 999px;
4088
4294
  color: var(--text-muted);
4295
+ display: inline-flex;
4296
+ gap: 5px;
4089
4297
  padding: 7px 10px;
4090
4298
  }
4091
4299
 
@@ -4103,7 +4311,7 @@ button.managed-toggle:focus-visible {
4103
4311
  border-radius: 12px;
4104
4312
  display: grid;
4105
4313
  gap: 6px;
4106
- grid-template-columns: minmax(0, 1.5fr) auto auto auto auto;
4314
+ grid-template-columns: minmax(0, 1.5fr) auto auto auto auto auto;
4107
4315
  padding: 10px;
4108
4316
  }
4109
4317
 
@@ -4126,7 +4334,8 @@ button.managed-toggle:focus-visible {
4126
4334
  }
4127
4335
 
4128
4336
  .managed-app-summary,
4129
- .managed-app-toolbar {
4337
+ .managed-app-toolbar,
4338
+ .provider-toolbar {
4130
4339
  grid-template-columns: 1fr;
4131
4340
  }
4132
4341
 
package/setup.sh CHANGED
@@ -12,7 +12,7 @@ shopt -s inherit_errexit 2>/dev/null || true
12
12
  # AI Assistant Server Access Framework Setup Script
13
13
  # Helps developers set up the framework for their infrastructure
14
14
  #
15
- # Version: 3.31.74
15
+ # Version: 3.31.76
16
16
  #
17
17
  # Quick Install:
18
18
  # npm install -g aidevops && aidevops update (recommended)