@hienlh/ppm 0.13.75 → 0.13.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/CHANGELOG.md +6 -0
- package/assets/skills/ppm/SKILL.md +1 -1
- package/assets/skills/ppm/references/cli-reference.md +4 -4
- package/assets/skills/ppm/references/http-api.md +7 -1
- package/bun.lock +2142 -0
- package/bunfig.toml +2 -0
- package/dist/web/assets/ai-settings-section-D0VMZ4aE.js +1 -0
- package/dist/web/assets/{api-settings-BJTjIG4U.js → api-settings-Byph7lae.js} +1 -1
- package/dist/web/assets/architecture-PBZL5I3N-DyeqgxGw.js +1 -0
- package/dist/web/assets/{audio-preview-BQUzmaMW.js → audio-preview-DXCxll7f.js} +1 -1
- package/dist/web/assets/chat-tab-D5j5gP5j.js +16 -0
- package/dist/web/assets/code-editor-CL2gcvDj.js +10 -0
- package/dist/web/assets/{conflict-editor-CrpXDFE1.js → conflict-editor-RMsNwUKp.js} +1 -1
- package/dist/web/assets/{csv-preview-asMfgR0r.js → csv-preview-CwEbP_iZ.js} +1 -1
- package/dist/web/assets/{data-grid-overlay-editor-DGjqvYn6.js → data-grid-overlay-editor-C36FRqE8.js} +1 -1
- package/dist/web/assets/database-viewer-2TbU0m7s.js +1 -0
- package/dist/web/assets/{diff-viewer-CdZ15fOk.js → diff-viewer--SSMwMoS.js} +1 -1
- package/dist/web/assets/{docx-preview-AgoO5zwo.js → docx-preview-BL398ELS.js} +1 -1
- package/dist/web/assets/{esm-xVTUq__o.js → esm-DH3rpl0I.js} +1 -1
- package/dist/web/assets/{extension-webview-C4D4PnO3.js → extension-webview-DrnDwQpM.js} +2 -2
- package/dist/web/assets/git-log-panel-u6ehykcl.js +1 -0
- package/dist/web/assets/gitGraph-HDMCJU4V-CZAWeLUi.js +1 -0
- package/dist/web/assets/{glide-data-grid-BfXnM2CC.js → glide-data-grid-Benw7NI4.js} +6 -6
- package/dist/web/assets/globe-CQ8NAYvi.js +1 -0
- package/dist/web/assets/{image-preview-CyL9Lj_O.js → image-preview-DeNH71Ez.js} +1 -1
- package/dist/web/assets/index-BS3CQIT3.js +27 -0
- package/dist/web/assets/info-3K5VOQVL-BqVOLnRc.js +1 -0
- package/dist/web/assets/{input-CArJe9WS.js → input-DSELw5zU.js} +1 -1
- package/dist/web/assets/keybindings-store-gFa7vUoe.js +1 -0
- package/dist/web/assets/{markdown-renderer-Cat4uT8B.js → markdown-renderer-D5B629qw.js} +3 -3
- package/dist/web/assets/notification-store-qViiZZaY.js +1 -0
- package/dist/web/assets/{number-overlay-editor-DtUBprPW.js → number-overlay-editor-JsUdft7z.js} +1 -1
- package/dist/web/assets/packet-RMMSAZCW-BGMrAgbD.js +1 -0
- package/dist/web/assets/{panel-store-C9VAhbZz.js → panel-store-DlvwzOll.js} +1 -1
- package/dist/web/assets/{pdf-preview-BSrz_N0g.js → pdf-preview-yUMARG8r.js} +1 -1
- package/dist/web/assets/pie-UPGHQEXC-9IRPAyAe.js +1 -0
- package/dist/web/assets/port-forwarding-tab-D7rVcasa.js +1 -0
- package/dist/web/assets/{postgres-viewer-GpXLzmnT.js → postgres-viewer-C28tRuh5.js} +3 -3
- package/dist/web/assets/{project-store-DlbHpIq0.js → project-store-CpC02pIv.js} +1 -1
- package/dist/web/assets/radar-KQ55EAFF-CxjdSb6M.js +1 -0
- package/dist/web/assets/refresh-cw-CRD2qr4U.js +1 -0
- package/dist/web/assets/{settings-store-DQUFTPk2.js → settings-store-MXJgFUnl.js} +2 -2
- package/dist/web/assets/settings-tab-0kWZtlCi.js +1 -0
- package/dist/web/assets/{sql-query-editor-PFN7evxv.js → sql-query-editor-DCx7kPlY.js} +1 -1
- package/dist/web/assets/sqlite-viewer-W1RhaCr0.js +1 -0
- package/dist/web/assets/system-monitor-tab-CJojQd3x.js +1 -0
- package/dist/web/assets/{tab-store-CIcbSn0c.js → tab-store-Bdw8DIbZ.js} +1 -1
- package/dist/web/assets/{terminal-tab-D2OwQv1h.js → terminal-tab-B1_sAuIi.js} +2 -2
- package/dist/web/assets/treemap-KZPCXAKY-BYrmfSj5.js +1 -0
- package/dist/web/assets/{use-blob-url-DrPfBQBM.js → use-blob-url-CBi0HMq5.js} +1 -1
- package/dist/web/assets/{use-monaco-theme-BLIgarH5.js → use-monaco-theme-CbzQcrwD.js} +1 -1
- package/dist/web/assets/{vendor-mermaid-DkqjpqJK.js → vendor-mermaid-D2cOxeao.js} +3 -3
- package/dist/web/assets/{video-preview-49zZk7VQ.js → video-preview-D-nAiQsv.js} +1 -1
- package/dist/web/index.html +20 -17
- package/dist/web/sw.js +1 -1
- package/package.json +1 -1
- package/src/index.ts +0 -0
- package/src/providers/claude-agent-sdk.ts +6 -2
- package/src/server/index.ts +2 -0
- package/src/server/routes/chat.ts +7 -6
- package/src/server/routes/push.ts +54 -0
- package/src/services/cloud-ws.service.ts +1 -27
- package/src/services/config.service.ts +1 -1
- package/src/services/db.service.ts +24 -0
- package/src/services/notification.service.ts +5 -14
- package/src/services/push-notification.service.ts +104 -0
- package/src/types/config.ts +7 -0
- package/src/web/components/chat/chat-tab.tsx +10 -0
- package/src/web/components/settings/settings-tab.tsx +66 -12
- package/src/web/hooks/use-push-notification.ts +114 -0
- package/src/web/sw.ts +45 -0
- package/.opencode/.env.example +0 -98
- package/.opencode/skills/ads-management/scripts/.env.example +0 -13
- package/.opencode/skills/ai-multimodal/.env.example +0 -230
- package/.opencode/skills/cip-design/.env.example +0 -6
- package/.opencode/skills/devops/.env.example +0 -76
- package/.opencode/skills/docs-seeker/.env.example +0 -15
- package/.opencode/skills/elevenlabs/.env.example +0 -3
- package/.opencode/skills/marketing-dashboard/.env.example +0 -15
- package/.opencode/skills/marketing-dashboard/app/.env.example +0 -2
- package/.opencode/skills/marketing-dashboard/server/.env.example +0 -2
- package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +0 -70
- package/.opencode/skills/mcp-management/scripts/dist/cli.js +0 -160
- package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +0 -183
- package/.opencode/skills/payment-integration/scripts/.env.example +0 -20
- package/.opencode/skills/sequential-thinking/.env.example +0 -8
- package/dist/web/assets/architecture-PBZL5I3N-CkdUQjA_.js +0 -1
- package/dist/web/assets/chat-tab-B80a21gx.js +0 -16
- package/dist/web/assets/code-editor-hFO6vAKT.js +0 -10
- package/dist/web/assets/database-viewer-CAjJc7bL.js +0 -1
- package/dist/web/assets/git-log-panel-J0pVWvZl.js +0 -1
- package/dist/web/assets/gitGraph-HDMCJU4V-D3UR56AG.js +0 -1
- package/dist/web/assets/index-Dd6YEaE6.js +0 -27
- package/dist/web/assets/info-3K5VOQVL-DUhLSKI2.js +0 -1
- package/dist/web/assets/keybindings-store-C1yHwKEc.js +0 -1
- package/dist/web/assets/notification-store-CBTVrgEf.js +0 -1
- package/dist/web/assets/packet-RMMSAZCW-BIpeVUGW.js +0 -1
- package/dist/web/assets/pie-UPGHQEXC-CNoizzjb.js +0 -1
- package/dist/web/assets/port-forwarding-tab-BpeFzbAM.js +0 -1
- package/dist/web/assets/radar-KQ55EAFF-7dns-ho5.js +0 -1
- package/dist/web/assets/settings-tab-Cjiu2egJ.js +0 -1
- package/dist/web/assets/sqlite-viewer-ONh1tmxh.js +0 -1
- package/dist/web/assets/system-monitor-tab-BznIBj8p.js +0 -1
- package/dist/web/assets/treemap-KZPCXAKY-D3DZCLoE.js +0 -1
- /package/dist/web/assets/{api-client-BK4NPNoY.js → api-client-DG9qwosT.js} +0 -0
- /package/dist/web/assets/{chevron-down-CiFNPrfI.js → chevron-down-BMo4cBth.js} +0 -0
- /package/dist/web/assets/{chevron-right-BzAdxJRG.js → chevron-right-CD8e6Aj4.js} +0 -0
- /package/dist/web/assets/{code-CuravVys.js → code-DiNmA3eR.js} +0 -0
- /package/dist/web/assets/{csv-parser-D1b_lg2T.js → csv-parser-B_TuHmnd.js} +0 -0
- /package/dist/web/assets/{data-grid-types-DzL5W2em.js → data-grid-types-CO_3iSwd.js} +0 -0
- /package/dist/web/assets/{database-NmqHg29g.js → database-Dc8mr-dP.js} +0 -0
- /package/dist/web/assets/{dist-CohudVKa.js → dist-C1jciI67.js} +0 -0
- /package/dist/web/assets/{dist-BM2EHhLH.js → dist-D4dFaZkK.js} +0 -0
- /package/dist/web/assets/{file-exclamation-point-Baz81y5z.js → file-exclamation-point-B__2Hrd6.js} +0 -0
- /package/dist/web/assets/{katex-CHaeM9QC.js → katex-DveWxdWJ.js} +0 -0
- /package/dist/web/assets/{lib-LPmTkMu4.js → lib-D4YDpYv4.js} +0 -0
- /package/dist/web/assets/{react-DHBl6KRc.js → react-BXxixfbh.js} +0 -0
- /package/dist/web/assets/{search-BEy08Exr.js → search-D90WJ5fo.js} +0 -0
- /package/dist/web/assets/{shield-check-77W0OMbn.js → shield-check-DeIMQtEj.js} +0 -0
- /package/dist/web/assets/{shield-off-C_MK1u09.js → shield-off-D4jBmG5E.js} +0 -0
- /package/dist/web/assets/{sparkles-CulWHe4c.js → sparkles-DyeiGE7Q.js} +0 -0
- /package/dist/web/assets/{table-BzjWcs87.js → table-DCYlHUNQ.js} +0 -0
- /package/dist/web/assets/{text-wrap-DJz9Bgpa.js → text-wrap-B3mYv9Yo.js} +0 -0
- /package/dist/web/assets/{trash-2-D5P4y8p_.js → trash-2-DkIfBY8d.js} +0 -0
- /package/dist/web/assets/{utils-CSCvNZxE.js → utils-Bs_TFEQf.js} +0 -0
- /package/dist/web/assets/{vendor-xterm-t3d5xZdz.js → vendor-xterm-BHJtSw6L.js} +0 -0
- /package/dist/web/assets/{wifi-CgM9T6HR.js → wifi-DifNnmbA.js} +0 -0
- /package/dist/web/assets/{x-Dx3jsRgu.js → x-WwAMX3EB.js} +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useState, useCallback, useRef } from "react";
|
|
2
2
|
import {
|
|
3
|
-
Moon, Sun, Monitor, Bell, Check, ChevronRight, ArrowLeft,
|
|
3
|
+
Moon, Sun, Monitor, Bell, BellOff, Check, ChevronRight, ArrowLeft,
|
|
4
4
|
Bot, BellRing, Keyboard, Globe, Plug, Puzzle, Bug, FolderSearch,
|
|
5
5
|
} from "lucide-react";
|
|
6
6
|
import { Button } from "@/components/ui/button";
|
|
@@ -19,12 +19,18 @@ import { ExtensionManagerSection } from "./extension-manager-section";
|
|
|
19
19
|
import { PPMBotSettingsSection } from "./ppmbot-settings-section";
|
|
20
20
|
import { ChangePasswordSection } from "./change-password-section";
|
|
21
21
|
import { FilesSettingsSection } from "./files-settings-section";
|
|
22
|
+
import { usePushNotification } from "@/hooks/use-push-notification";
|
|
23
|
+
|
|
22
24
|
const THEME_OPTIONS: { value: Theme; label: string; icon: React.ElementType }[] = [
|
|
23
25
|
{ value: "light", label: "Light", icon: Sun },
|
|
24
26
|
{ value: "dark", label: "Dark", icon: Moon },
|
|
25
27
|
{ value: "system", label: "System", icon: Monitor },
|
|
26
28
|
];
|
|
27
29
|
|
|
30
|
+
const pushSupported = "PushManager" in window && "serviceWorker" in navigator;
|
|
31
|
+
const isIosNonPwa = /iPhone|iPad/.test(navigator.userAgent) &&
|
|
32
|
+
!window.matchMedia("(display-mode: standalone)").matches;
|
|
33
|
+
|
|
28
34
|
type SettingsCategory = "ai" | "notifications" | "clawbot" | "jira" | "proxy" | "shortcuts" | "mcp" | "extensions" | "files";
|
|
29
35
|
|
|
30
36
|
const CATEGORIES: { value: SettingsCategory; label: string; subtitle: string; icon: React.ElementType }[] = [
|
|
@@ -41,6 +47,7 @@ const CATEGORIES: { value: SettingsCategory; label: string; subtitle: string; ic
|
|
|
41
47
|
|
|
42
48
|
export function SettingsTab() {
|
|
43
49
|
const { theme, setTheme, deviceName, setDeviceName, version, jiraEnabled, setJiraEnabled } = useSettingsStore(useShallow((s) => ({ theme: s.theme, setTheme: s.setTheme, deviceName: s.deviceName, setDeviceName: s.setDeviceName, version: s.version, jiraEnabled: s.jiraEnabled, setJiraEnabled: s.setJiraEnabled })));
|
|
50
|
+
const { permission, isSubscribed, loading, error: pushError, subscribe, unsubscribe } = usePushNotification();
|
|
44
51
|
const [activeCategory, setActiveCategory] = useState<SettingsCategory | null>(null);
|
|
45
52
|
const [nameInput, setNameInput] = useState(deviceName ?? "");
|
|
46
53
|
const [nameSaving, setNameSaving] = useState(false);
|
|
@@ -85,7 +92,7 @@ export function SettingsTab() {
|
|
|
85
92
|
<ScrollArea className="flex-1 min-h-0">
|
|
86
93
|
<div className="p-3">
|
|
87
94
|
{activeCategory === "ai" && <AISettingsSection compact />}
|
|
88
|
-
{activeCategory === "notifications" && <NotificationsContent />}
|
|
95
|
+
{activeCategory === "notifications" && <NotificationsContent isSubscribed={isSubscribed} loading={loading} permission={permission} pushError={pushError} subscribe={subscribe} unsubscribe={unsubscribe} />}
|
|
89
96
|
{activeCategory === "clawbot" && <PPMBotSettingsSection />}
|
|
90
97
|
{/* Jira is now a sidebar tab with a toggle below */}
|
|
91
98
|
{activeCategory === "proxy" && <ProxySettingsSection />}
|
|
@@ -224,20 +231,67 @@ export function SettingsTab() {
|
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
/** Notifications detail content — extracted to keep SettingsTab clean */
|
|
227
|
-
function NotificationsContent(
|
|
234
|
+
function NotificationsContent({ isSubscribed, loading, permission, pushError, subscribe, unsubscribe }: {
|
|
235
|
+
isSubscribed: boolean;
|
|
236
|
+
loading: boolean;
|
|
237
|
+
permission: NotificationPermission;
|
|
238
|
+
pushError: string | null;
|
|
239
|
+
subscribe: () => void;
|
|
240
|
+
unsubscribe: () => void;
|
|
241
|
+
}) {
|
|
228
242
|
return (
|
|
229
243
|
<div className="space-y-4">
|
|
230
|
-
{/*
|
|
244
|
+
{/* Push */}
|
|
231
245
|
<section className="space-y-2">
|
|
232
246
|
<h3 className="text-xs font-medium text-muted-foreground">Push Notifications</h3>
|
|
233
|
-
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
247
|
+
{!pushSupported ? (
|
|
248
|
+
<p className="text-[11px] text-muted-foreground">
|
|
249
|
+
Push notifications not supported in this browser.
|
|
250
|
+
</p>
|
|
251
|
+
) : (
|
|
252
|
+
<div className="space-y-2">
|
|
253
|
+
<div className="flex items-center justify-between">
|
|
254
|
+
<div className="flex items-center gap-1.5">
|
|
255
|
+
{isSubscribed ? <Bell className="size-3.5" /> : <BellOff className="size-3.5" />}
|
|
256
|
+
<span className="text-xs">Push notifications</span>
|
|
257
|
+
</div>
|
|
258
|
+
<Button
|
|
259
|
+
variant={isSubscribed ? "default" : "outline"}
|
|
260
|
+
size="sm"
|
|
261
|
+
className="h-7 text-xs cursor-pointer"
|
|
262
|
+
disabled={loading || permission === "denied"}
|
|
263
|
+
onClick={() => (isSubscribed ? unsubscribe() : subscribe())}
|
|
264
|
+
>
|
|
265
|
+
{loading ? "..." : isSubscribed ? "On" : "Off"}
|
|
266
|
+
</Button>
|
|
267
|
+
</div>
|
|
268
|
+
{isSubscribed && (
|
|
269
|
+
<Button
|
|
270
|
+
variant="outline"
|
|
271
|
+
size="sm"
|
|
272
|
+
className="h-7 text-xs w-full cursor-pointer"
|
|
273
|
+
onClick={() => {
|
|
274
|
+
new Notification("PPM Test", { body: "Push notifications are working!" });
|
|
275
|
+
}}
|
|
276
|
+
>
|
|
277
|
+
Test notification
|
|
278
|
+
</Button>
|
|
279
|
+
)}
|
|
280
|
+
{pushError && (
|
|
281
|
+
<p className="text-[11px] text-destructive">{pushError}</p>
|
|
282
|
+
)}
|
|
283
|
+
{permission === "denied" && (
|
|
284
|
+
<p className="text-[11px] text-destructive">
|
|
285
|
+
Notifications blocked. Enable in browser settings.
|
|
286
|
+
</p>
|
|
287
|
+
)}
|
|
288
|
+
{isIosNonPwa && (
|
|
289
|
+
<p className="text-[11px] text-muted-foreground">
|
|
290
|
+
On iOS, install PPM to Home Screen for push notifications.
|
|
291
|
+
</p>
|
|
292
|
+
)}
|
|
293
|
+
</div>
|
|
294
|
+
)}
|
|
241
295
|
</section>
|
|
242
296
|
|
|
243
297
|
<Separator />
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useState, useEffect, useCallback } from "react";
|
|
2
|
+
import { getAuthToken } from "@/lib/api-client";
|
|
3
|
+
|
|
4
|
+
/** Convert VAPID public key from base64url to Uint8Array for PushManager */
|
|
5
|
+
function urlBase64ToUint8Array(base64String: string): Uint8Array {
|
|
6
|
+
const padding = "=".repeat((4 - (base64String.length % 4)) % 4);
|
|
7
|
+
const base64 = (base64String + padding).replace(/-/g, "+").replace(/_/g, "/");
|
|
8
|
+
const rawData = atob(base64);
|
|
9
|
+
return Uint8Array.from(rawData, (char) => char.charCodeAt(0));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function usePushNotification() {
|
|
13
|
+
const [permission, setPermission] = useState<NotificationPermission>("default");
|
|
14
|
+
const [isSubscribed, setIsSubscribed] = useState(false);
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState<string | null>(null);
|
|
17
|
+
|
|
18
|
+
// Check current permission and subscription state on mount
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if ("Notification" in window) {
|
|
21
|
+
setPermission(Notification.permission);
|
|
22
|
+
}
|
|
23
|
+
setIsSubscribed(localStorage.getItem("ppm-push-subscribed") === "true");
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const subscribe = useCallback(async () => {
|
|
27
|
+
setLoading(true);
|
|
28
|
+
setError(null);
|
|
29
|
+
try {
|
|
30
|
+
// 1. Request notification permission
|
|
31
|
+
const perm = await Notification.requestPermission();
|
|
32
|
+
setPermission(perm);
|
|
33
|
+
if (perm !== "granted") {
|
|
34
|
+
setError("Permission denied");
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 2. Check service worker is available (with timeout)
|
|
39
|
+
if (!navigator.serviceWorker?.controller && !navigator.serviceWorker?.ready) {
|
|
40
|
+
setError("Service worker not available (dev mode?)");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const swReady = await Promise.race([
|
|
45
|
+
navigator.serviceWorker.ready,
|
|
46
|
+
new Promise<null>((_, reject) => setTimeout(() => reject(new Error("Service worker timeout")), 5000)),
|
|
47
|
+
]);
|
|
48
|
+
if (!swReady) throw new Error("Service worker not ready");
|
|
49
|
+
|
|
50
|
+
// 3. Get VAPID public key from server
|
|
51
|
+
const headers: Record<string, string> = {};
|
|
52
|
+
const token = getAuthToken();
|
|
53
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
54
|
+
|
|
55
|
+
const res = await fetch("/api/push/vapid-key", { headers });
|
|
56
|
+
const json = await res.json();
|
|
57
|
+
if (!json.ok) throw new Error(json.error || "Failed to get VAPID key");
|
|
58
|
+
|
|
59
|
+
// 4. Subscribe via PushManager
|
|
60
|
+
const sub = await swReady.pushManager.subscribe({
|
|
61
|
+
userVisibleOnly: true,
|
|
62
|
+
applicationServerKey: urlBase64ToUint8Array(json.data.publicKey).buffer as ArrayBuffer,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// 5. Send subscription to server
|
|
66
|
+
await fetch("/api/push/subscribe", {
|
|
67
|
+
method: "POST",
|
|
68
|
+
headers: { ...headers, "Content-Type": "application/json" },
|
|
69
|
+
body: JSON.stringify(sub.toJSON()),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
setIsSubscribed(true);
|
|
73
|
+
localStorage.setItem("ppm-push-subscribed", "true");
|
|
74
|
+
} catch (err) {
|
|
75
|
+
const msg = err instanceof Error ? err.message : "Subscribe failed";
|
|
76
|
+
setError(msg);
|
|
77
|
+
console.error("[push] Subscribe failed:", err);
|
|
78
|
+
} finally {
|
|
79
|
+
setLoading(false);
|
|
80
|
+
}
|
|
81
|
+
}, []);
|
|
82
|
+
|
|
83
|
+
const unsubscribe = useCallback(async () => {
|
|
84
|
+
setLoading(true);
|
|
85
|
+
try {
|
|
86
|
+
const reg = await navigator.serviceWorker.ready;
|
|
87
|
+
const sub = await reg.pushManager.getSubscription();
|
|
88
|
+
if (sub) {
|
|
89
|
+
// Remove from server
|
|
90
|
+
const headers: Record<string, string> = { "Content-Type": "application/json" };
|
|
91
|
+
const token = getAuthToken();
|
|
92
|
+
if (token) headers.Authorization = `Bearer ${token}`;
|
|
93
|
+
|
|
94
|
+
await fetch("/api/push/subscribe", {
|
|
95
|
+
method: "DELETE",
|
|
96
|
+
headers,
|
|
97
|
+
body: JSON.stringify({ endpoint: sub.endpoint }),
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Unsubscribe locally
|
|
101
|
+
await sub.unsubscribe();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
setIsSubscribed(false);
|
|
105
|
+
localStorage.removeItem("ppm-push-subscribed");
|
|
106
|
+
} catch (err) {
|
|
107
|
+
console.error("[push] Unsubscribe failed:", err);
|
|
108
|
+
} finally {
|
|
109
|
+
setLoading(false);
|
|
110
|
+
}
|
|
111
|
+
}, []);
|
|
112
|
+
|
|
113
|
+
return { permission, isSubscribed, loading, error, subscribe, unsubscribe };
|
|
114
|
+
}
|
package/src/web/sw.ts
CHANGED
|
@@ -5,3 +5,48 @@ declare const self: ServiceWorkerGlobalScope;
|
|
|
5
5
|
|
|
6
6
|
// Workbox injects precache manifest here
|
|
7
7
|
precacheAndRoute(self.__WB_MANIFEST);
|
|
8
|
+
|
|
9
|
+
// Handle push notifications from server
|
|
10
|
+
self.addEventListener("push", (event) => {
|
|
11
|
+
event.waitUntil(
|
|
12
|
+
self.clients
|
|
13
|
+
.matchAll({ type: "window", includeUncontrolled: true })
|
|
14
|
+
.then((clients) => {
|
|
15
|
+
// Skip notification if any PPM tab is currently visible
|
|
16
|
+
const hasVisibleClient = clients.some(
|
|
17
|
+
(c) => c.visibilityState === "visible",
|
|
18
|
+
);
|
|
19
|
+
if (hasVisibleClient) return;
|
|
20
|
+
|
|
21
|
+
const data = event.data?.json() ?? {
|
|
22
|
+
title: "PPM",
|
|
23
|
+
body: "Chat completed",
|
|
24
|
+
};
|
|
25
|
+
return self.registration.showNotification(data.title, {
|
|
26
|
+
body: data.body,
|
|
27
|
+
icon: "/icon-192.svg",
|
|
28
|
+
badge: "/icon-192.svg",
|
|
29
|
+
tag: "ppm-chat-done",
|
|
30
|
+
silent: false,
|
|
31
|
+
data: { url: self.location.origin },
|
|
32
|
+
});
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Handle notification click — focus existing tab or open new one
|
|
38
|
+
self.addEventListener("notificationclick", (event) => {
|
|
39
|
+
event.notification.close();
|
|
40
|
+
event.waitUntil(
|
|
41
|
+
self.clients
|
|
42
|
+
.matchAll({ type: "window", includeUncontrolled: true })
|
|
43
|
+
.then((clients) => {
|
|
44
|
+
for (const client of clients) {
|
|
45
|
+
if (client.url.includes(self.location.origin) && "focus" in client) {
|
|
46
|
+
return client.focus();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return self.clients.openWindow(event.notification.data?.url || "/");
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
});
|
package/.opencode/.env.example
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# Claude Code - Global Environment Variables
|
|
2
|
-
# Location: .claude/.env
|
|
3
|
-
# Priority: LOWEST (overridden by skills/.env and skill-specific .env)
|
|
4
|
-
# Scope: Project-wide configuration, global defaults
|
|
5
|
-
# Setup: Copy to .claude/.env and configure
|
|
6
|
-
|
|
7
|
-
# ============================================
|
|
8
|
-
# Environment Variable Hierarchy
|
|
9
|
-
# ============================================
|
|
10
|
-
# Priority order (highest to lowest):
|
|
11
|
-
# 1. process.env - Runtime environment (HIGHEST)
|
|
12
|
-
# 2. .claude/skills/<skill>/.env - Skill-specific overrides
|
|
13
|
-
# 3. .claude/skills/.env - Shared across all skills
|
|
14
|
-
# 4. .claude/.env - Global defaults (this file, LOWEST)
|
|
15
|
-
#
|
|
16
|
-
# All skills use centralized resolver: ~/.claude/scripts/resolve_env.py
|
|
17
|
-
# Debug hierarchy: python ~/.claude/scripts/resolve_env.py --show-hierarchy
|
|
18
|
-
|
|
19
|
-
# ============================================
|
|
20
|
-
# ClaudeKit API Key (for VidCap, ReviewWeb services)
|
|
21
|
-
# ============================================
|
|
22
|
-
# Get your API key from https://claudekit.cc/api-keys
|
|
23
|
-
# Required for accessing ClaudeKit services via skills
|
|
24
|
-
CLAUDEKIT_API_KEY=
|
|
25
|
-
|
|
26
|
-
# ============================================
|
|
27
|
-
# Claude Code Notification Hooks
|
|
28
|
-
# ============================================
|
|
29
|
-
# Discord Webhook URL (for Discord notifications)
|
|
30
|
-
# Get from: Server Settings → Integrations → Webhooks → New Webhook
|
|
31
|
-
DISCORD_WEBHOOK_URL=
|
|
32
|
-
|
|
33
|
-
# Telegram Bot Token (for Telegram notifications)
|
|
34
|
-
# Get from: @BotFather in Telegram
|
|
35
|
-
TELEGRAM_BOT_TOKEN=
|
|
36
|
-
|
|
37
|
-
# Telegram Chat ID (your chat ID or group ID)
|
|
38
|
-
# Get from: https://api.telegram.org/bot<BOT_TOKEN>/getUpdates
|
|
39
|
-
TELEGRAM_CHAT_ID=
|
|
40
|
-
|
|
41
|
-
# ============================================
|
|
42
|
-
# AI/ML API Keys (Global Defaults)
|
|
43
|
-
# ============================================
|
|
44
|
-
# Google Gemini API (for ai-multimodal, docs-seeker skills)
|
|
45
|
-
# Get from: https://aistudio.google.com/apikey
|
|
46
|
-
GEMINI_API_KEY=
|
|
47
|
-
|
|
48
|
-
# Vertex AI Configuration (Optional alternative to AI Studio)
|
|
49
|
-
# GEMINI_USE_VERTEX=true
|
|
50
|
-
# VERTEX_PROJECT_ID=
|
|
51
|
-
# VERTEX_LOCATION=us-central1
|
|
52
|
-
|
|
53
|
-
# OpenAI API Key (if using OpenAI-based skills)
|
|
54
|
-
# OPENAI_API_KEY=
|
|
55
|
-
|
|
56
|
-
# Anthropic API Key (if using Claude API directly)
|
|
57
|
-
# ANTHROPIC_API_KEY=
|
|
58
|
-
|
|
59
|
-
# ElevenLabs API Key
|
|
60
|
-
# Get your key at: https://elevenlabs.io/app/settings/api-keys
|
|
61
|
-
# ELEVENLABS_API_KEY=
|
|
62
|
-
|
|
63
|
-
# ============================================
|
|
64
|
-
# Development & CI/CD
|
|
65
|
-
# ============================================
|
|
66
|
-
# NODE_ENV=development
|
|
67
|
-
# DEBUG=false
|
|
68
|
-
# LOG_LEVEL=info
|
|
69
|
-
|
|
70
|
-
# ============================================
|
|
71
|
-
# Project Configuration
|
|
72
|
-
# ============================================
|
|
73
|
-
# PROJECT_NAME=claudekit-engineer
|
|
74
|
-
# ENVIRONMENT=local
|
|
75
|
-
|
|
76
|
-
# ============================================
|
|
77
|
-
# Example Usage Scenarios
|
|
78
|
-
# ============================================
|
|
79
|
-
# Scenario 1: Global default for all skills
|
|
80
|
-
# .claude/.env (this file): GEMINI_API_KEY=global-dev-key
|
|
81
|
-
# Result: All skills use global-dev-key
|
|
82
|
-
#
|
|
83
|
-
# Scenario 2: Override for all skills
|
|
84
|
-
# .claude/.env (this file): GEMINI_API_KEY=global-dev-key
|
|
85
|
-
# .claude/skills/.env: GEMINI_API_KEY=skills-prod-key
|
|
86
|
-
# Result: All skills use skills-prod-key
|
|
87
|
-
#
|
|
88
|
-
# Scenario 3: Skill-specific override
|
|
89
|
-
# .claude/.env (this file): GEMINI_API_KEY=global-key
|
|
90
|
-
# .claude/skills/.env: GEMINI_API_KEY=shared-key
|
|
91
|
-
# .claude/skills/ai-multimodal/.env: GEMINI_API_KEY=high-quota-key
|
|
92
|
-
# Result: ai-multimodal uses high-quota-key, other skills use shared-key
|
|
93
|
-
#
|
|
94
|
-
# Scenario 4: Runtime testing
|
|
95
|
-
# export GEMINI_API_KEY=test-key
|
|
96
|
-
# Result: All skills use test-key regardless of config files
|
|
97
|
-
#
|
|
98
|
-
# Priority: runtime > skill-specific > shared > global (this file)
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# Google Ads API credentials
|
|
2
|
-
GOOGLE_ADS_DEVELOPER_TOKEN=your_developer_token
|
|
3
|
-
GOOGLE_ADS_CLIENT_ID=your_client_id.apps.googleusercontent.com
|
|
4
|
-
GOOGLE_ADS_CLIENT_SECRET=your_client_secret
|
|
5
|
-
GOOGLE_ADS_REFRESH_TOKEN=1//your_refresh_token
|
|
6
|
-
GOOGLE_ADS_CUSTOMER_ID=1234567890
|
|
7
|
-
GOOGLE_ADS_LOGIN_CUSTOMER_ID=your_mcc_id
|
|
8
|
-
|
|
9
|
-
# Meta/Facebook Ads API credentials
|
|
10
|
-
META_APP_ID=your_app_id
|
|
11
|
-
META_APP_SECRET=your_app_secret
|
|
12
|
-
META_ACCESS_TOKEN=your_system_user_token
|
|
13
|
-
META_AD_ACCOUNT_ID=act_123456789
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
# Google Gemini API Configuration
|
|
2
|
-
|
|
3
|
-
# ============================================================================
|
|
4
|
-
# OPTION 1: Google AI Studio (Default - Recommended for most users)
|
|
5
|
-
# ============================================================================
|
|
6
|
-
# Get your API key: https://aistudio.google.com/apikey
|
|
7
|
-
GEMINI_API_KEY=your_api_key_here
|
|
8
|
-
|
|
9
|
-
# ============================================================================
|
|
10
|
-
# API Key Rotation (Optional - For high-volume usage)
|
|
11
|
-
# ============================================================================
|
|
12
|
-
# Add multiple API keys for automatic rotation on rate limit errors.
|
|
13
|
-
# Free tier accounts are heavily rate-limited; rotation helps distribute load.
|
|
14
|
-
#
|
|
15
|
-
# Format: GEMINI_API_KEY_N where N is 2, 3, 4, etc.
|
|
16
|
-
# The primary GEMINI_API_KEY is always used first.
|
|
17
|
-
#
|
|
18
|
-
# GEMINI_API_KEY_2=your_second_api_key
|
|
19
|
-
# GEMINI_API_KEY_3=your_third_api_key
|
|
20
|
-
# GEMINI_API_KEY_4=your_fourth_api_key
|
|
21
|
-
#
|
|
22
|
-
# Features:
|
|
23
|
-
# - Auto-rotates on RESOURCE_EXHAUSTED / 429 errors
|
|
24
|
-
# - 60-second cooldown per key after rate limit
|
|
25
|
-
# - Logs rotation events with --verbose flag
|
|
26
|
-
# - Backward compatible: single key still works
|
|
27
|
-
|
|
28
|
-
# ============================================================================
|
|
29
|
-
# OPTION 2: Vertex AI (Google Cloud Platform)
|
|
30
|
-
# ============================================================================
|
|
31
|
-
# Uncomment these lines to use Vertex AI instead of Google AI Studio
|
|
32
|
-
# GEMINI_USE_VERTEX=true
|
|
33
|
-
# VERTEX_PROJECT_ID=your-gcp-project-id
|
|
34
|
-
# VERTEX_LOCATION=us-central1
|
|
35
|
-
|
|
36
|
-
# ============================================================================
|
|
37
|
-
# Model Selection (Optional)
|
|
38
|
-
# ============================================================================
|
|
39
|
-
# Override default models for specific capabilities
|
|
40
|
-
# If not set, intelligent defaults are used based on task type
|
|
41
|
-
|
|
42
|
-
# --- Image Generation ---
|
|
43
|
-
# Used by: --task generate (image)
|
|
44
|
-
# Default: gemini-2.5-flash-image (Nano Banana Flash - fast, cost-effective)
|
|
45
|
-
# Alternative: imagen-4.0-generate-001 (production quality)
|
|
46
|
-
# NOTE: All image generation requires billing - no free tier available (limit: 0)
|
|
47
|
-
# Options:
|
|
48
|
-
# gemini-2.5-flash-image - Nano Banana Flash: fast, ~$1/1M tokens (DEFAULT)
|
|
49
|
-
# gemini-3-pro-image-preview - Nano Banana Pro: 4K text, reasoning (requires billing)
|
|
50
|
-
# imagen-4.0-generate-001 - Imagen 4 Standard: production quality (~$0.02/image)
|
|
51
|
-
# imagen-4.0-ultra-generate-001 - Imagen 4 Ultra: maximum quality (~$0.04/image)
|
|
52
|
-
# imagen-4.0-fast-generate-001 - Imagen 4 Fast: speed-optimized (~$0.01/image)
|
|
53
|
-
# IMAGE_GEN_MODEL=gemini-2.5-flash-image
|
|
54
|
-
|
|
55
|
-
# --- Video Generation ---
|
|
56
|
-
# Used by: --task generate-video (new capability)
|
|
57
|
-
# Default: veo-3.1-generate-preview
|
|
58
|
-
# NOTE: Video generation requires billing - no free tier fallback available
|
|
59
|
-
# Options:
|
|
60
|
-
# veo-3.1-generate-preview - Latest, native audio, frame control (requires billing)
|
|
61
|
-
# veo-3.1-fast-generate-preview - Speed-optimized for business (requires billing)
|
|
62
|
-
# veo-3.0-generate-001 - Stable, native audio, 8s videos (requires billing)
|
|
63
|
-
# veo-3.0-fast-generate-001 - Stable fast variant (requires billing)
|
|
64
|
-
# VIDEO_GEN_MODEL=veo-3.1-generate-preview
|
|
65
|
-
|
|
66
|
-
# --- Multimodal Analysis ---
|
|
67
|
-
# Used by: --task analyze, transcribe, extract
|
|
68
|
-
# Default: gemini-2.5-flash
|
|
69
|
-
# Options:
|
|
70
|
-
# gemini-3-pro-preview - Latest, agentic workflows, 1M context
|
|
71
|
-
# gemini-2.5-flash - Best price/performance (recommended)
|
|
72
|
-
# gemini-2.5-pro - Highest quality
|
|
73
|
-
# MULTIMODAL_MODEL=gemini-2.5-flash
|
|
74
|
-
|
|
75
|
-
# --- Legacy Compatibility ---
|
|
76
|
-
# Generic model override (use specific variables above instead)
|
|
77
|
-
# GEMINI_MODEL=gemini-2.5-flash
|
|
78
|
-
# GEMINI_IMAGE_GEN_MODEL=gemini-2.5-flash-image
|
|
79
|
-
|
|
80
|
-
# ============================================================================
|
|
81
|
-
# MiniMax API Configuration (Optional - for image/video/speech/music generation)
|
|
82
|
-
# ============================================================================
|
|
83
|
-
# Get your API key: https://platform.minimax.io/user-center/basic-information/interface-key
|
|
84
|
-
# MINIMAX_API_KEY=your_minimax_api_key_here
|
|
85
|
-
|
|
86
|
-
# --- MiniMax Image Generation ---
|
|
87
|
-
# Models: image-01 (standard), image-01-live (enhanced)
|
|
88
|
-
# Cost: ~$0.03/image | Rate: 10 RPM
|
|
89
|
-
# MINIMAX_IMAGE_MODEL=image-01
|
|
90
|
-
|
|
91
|
-
# --- MiniMax Video Generation (Hailuo) ---
|
|
92
|
-
# Models: MiniMax-Hailuo-2.3, MiniMax-Hailuo-2.3-Fast, MiniMax-Hailuo-02, S2V-01
|
|
93
|
-
# Cost: $0.25-0.52/video | Rate: 5 RPM
|
|
94
|
-
# MINIMAX_VIDEO_MODEL=MiniMax-Hailuo-2.3
|
|
95
|
-
|
|
96
|
-
# --- MiniMax Speech/TTS ---
|
|
97
|
-
# Models: speech-2.8-hd (best), speech-2.8-turbo (fast)
|
|
98
|
-
# Cost: $30-50/1M chars | Rate: 60 RPM | 300+ voices, 40+ languages
|
|
99
|
-
# MINIMAX_SPEECH_MODEL=speech-2.8-hd
|
|
100
|
-
|
|
101
|
-
# --- MiniMax Music Generation ---
|
|
102
|
-
# Models: music-2.5 (4-minute songs with vocals)
|
|
103
|
-
# Cost: $0.03-0.075/gen | Rate: 120 RPM
|
|
104
|
-
# MINIMAX_MUSIC_MODEL=music-2.5
|
|
105
|
-
|
|
106
|
-
# ============================================================================
|
|
107
|
-
# Rate Limiting Configuration (Optional)
|
|
108
|
-
# ============================================================================
|
|
109
|
-
# Requests per minute limit (adjust based on your tier)
|
|
110
|
-
# GEMINI_RPM_LIMIT=15
|
|
111
|
-
|
|
112
|
-
# Tokens per minute limit
|
|
113
|
-
# GEMINI_TPM_LIMIT=4000000
|
|
114
|
-
|
|
115
|
-
# Requests per day limit
|
|
116
|
-
# GEMINI_RPD_LIMIT=1500
|
|
117
|
-
|
|
118
|
-
# ============================================================================
|
|
119
|
-
# Video Generation Options (Optional)
|
|
120
|
-
# ============================================================================
|
|
121
|
-
# Video duration in seconds (8s only for now)
|
|
122
|
-
# VEO_DURATION=8
|
|
123
|
-
|
|
124
|
-
# Video resolution: 720p or 1080p
|
|
125
|
-
# VEO_RESOLUTION=1080p
|
|
126
|
-
|
|
127
|
-
# Aspect ratio: 16:9, 9:16, 1:1 (16:9 is default)
|
|
128
|
-
# VEO_ASPECT_RATIO=16:9
|
|
129
|
-
|
|
130
|
-
# Frame rate: 24fps (fixed for now)
|
|
131
|
-
# VEO_FPS=24
|
|
132
|
-
|
|
133
|
-
# Enable native audio generation
|
|
134
|
-
# VEO_AUDIO=true
|
|
135
|
-
|
|
136
|
-
# ============================================================================
|
|
137
|
-
# Image Generation Options (Optional)
|
|
138
|
-
# ============================================================================
|
|
139
|
-
# Number of images to generate (1-4)
|
|
140
|
-
# IMAGEN_NUM_IMAGES=1
|
|
141
|
-
|
|
142
|
-
# Image size: 1K or 2K (Ultra/Standard only)
|
|
143
|
-
# IMAGEN_SIZE=1K
|
|
144
|
-
|
|
145
|
-
# Aspect ratio: 1:1, 16:9, 9:16, 4:3, 3:4
|
|
146
|
-
# IMAGEN_ASPECT_RATIO=1:1
|
|
147
|
-
|
|
148
|
-
# Enable person generation (restricted in EEA, CH, UK)
|
|
149
|
-
# IMAGEN_PERSON_GENERATION=true
|
|
150
|
-
|
|
151
|
-
# Add SynthID watermark (always enabled by default)
|
|
152
|
-
# IMAGEN_WATERMARK=true
|
|
153
|
-
|
|
154
|
-
# ============================================================================
|
|
155
|
-
# Processing Options (Optional)
|
|
156
|
-
# ============================================================================
|
|
157
|
-
# Video resolution mode: default or low-res
|
|
158
|
-
# low-res uses ~100 tokens/second vs ~300 for default
|
|
159
|
-
# GEMINI_VIDEO_RESOLUTION=default
|
|
160
|
-
|
|
161
|
-
# Audio quality: default (16 Kbps mono, auto-downsampled)
|
|
162
|
-
# GEMINI_AUDIO_QUALITY=default
|
|
163
|
-
|
|
164
|
-
# PDF processing mode: inline (<20MB) or file-api (>20MB, automatic)
|
|
165
|
-
# GEMINI_PDF_MODE=auto
|
|
166
|
-
|
|
167
|
-
# ============================================================================
|
|
168
|
-
# Retry Configuration (Optional)
|
|
169
|
-
# ============================================================================
|
|
170
|
-
# Maximum retry attempts for failed requests
|
|
171
|
-
# GEMINI_MAX_RETRIES=3
|
|
172
|
-
|
|
173
|
-
# Initial retry delay in seconds (uses exponential backoff)
|
|
174
|
-
# GEMINI_RETRY_DELAY=1
|
|
175
|
-
|
|
176
|
-
# ============================================================================
|
|
177
|
-
# Output Configuration (Optional)
|
|
178
|
-
# ============================================================================
|
|
179
|
-
# Default output directory for generated images
|
|
180
|
-
# OUTPUT_DIR=./output
|
|
181
|
-
|
|
182
|
-
# Image output format (png or jpeg)
|
|
183
|
-
# IMAGE_FORMAT=png
|
|
184
|
-
|
|
185
|
-
# Image quality for JPEG (1-100)
|
|
186
|
-
# IMAGE_QUALITY=95
|
|
187
|
-
|
|
188
|
-
# ============================================================================
|
|
189
|
-
# Context Caching (Optional)
|
|
190
|
-
# ============================================================================
|
|
191
|
-
# Enable context caching for repeated queries on same file
|
|
192
|
-
# GEMINI_ENABLE_CACHING=true
|
|
193
|
-
|
|
194
|
-
# Cache TTL in seconds (default: 1800 = 30 minutes)
|
|
195
|
-
# GEMINI_CACHE_TTL=1800
|
|
196
|
-
|
|
197
|
-
# ============================================================================
|
|
198
|
-
# Logging (Optional)
|
|
199
|
-
# ============================================================================
|
|
200
|
-
# Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
201
|
-
# LOG_LEVEL=INFO
|
|
202
|
-
|
|
203
|
-
# Log file path
|
|
204
|
-
# LOG_FILE=./logs/gemini.log
|
|
205
|
-
|
|
206
|
-
# ============================================================================
|
|
207
|
-
# Pricing Reference (as of 2025-11)
|
|
208
|
-
# ============================================================================
|
|
209
|
-
# Gemini 2.5 Flash: $1.00/1M input, $0.10/1M output
|
|
210
|
-
# Gemini 2.5 Pro: $3.00/1M input, $12.00/1M output
|
|
211
|
-
# Gemini 3 Pro: $2.00/1M input (<200k), $4.00 (>200k), $12/$18 output
|
|
212
|
-
# Imagen 4: ~$0.01-$0.04 per image (varies by variant)
|
|
213
|
-
# Veo 3: TBD (preview pricing)
|
|
214
|
-
# Monitor: https://ai.google.dev/pricing
|
|
215
|
-
|
|
216
|
-
# ============================================================================
|
|
217
|
-
# Notes
|
|
218
|
-
# ============================================================================
|
|
219
|
-
# 1. Never commit API keys to version control
|
|
220
|
-
# 2. Add .env to .gitignore
|
|
221
|
-
# 3. API keys can be restricted in Google Cloud Console
|
|
222
|
-
# 4. Monitor usage at: https://aistudio.google.com/apikey
|
|
223
|
-
# 5. Free tier limits: 15 RPM, 1M-4M TPM, 1,500 RPD
|
|
224
|
-
# 6. Vertex AI requires GCP authentication via gcloud CLI
|
|
225
|
-
# 7. Model defaults (Dec 2025):
|
|
226
|
-
# - Image gen: gemini-2.5-flash-image (Nano Banana Flash - default)
|
|
227
|
-
# - Image gen: imagen-4.0-generate-001 (alternative for production)
|
|
228
|
-
# - Video gen: veo-3.1-generate-preview
|
|
229
|
-
# - Analysis: gemini-2.5-flash
|
|
230
|
-
# 8. Preview models (veo-3.1, gemini-3) may have API changes
|