@hienlh/ppm 0.9.82 → 0.9.83

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 (41) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/dist/web/assets/{chat-tab-bS86TsT5.js → chat-tab-DBJJz0Dm.js} +1 -1
  3. package/dist/web/assets/{code-editor-BaNaQ33b.js → code-editor-BvSFsrGo.js} +1 -1
  4. package/dist/web/assets/{database-viewer-C5MVw8cJ.js → database-viewer-0P_zRC9w.js} +1 -1
  5. package/dist/web/assets/{diff-viewer-CUbFMWVo.js → diff-viewer-BmBJq4gO.js} +1 -1
  6. package/dist/web/assets/{extension-webview-CwGufYEP.js → extension-webview-Cx0GpRyC.js} +1 -1
  7. package/dist/web/assets/{git-graph-BD7A7MLo.js → git-graph-BAlhf058.js} +1 -1
  8. package/dist/web/assets/{index-CpzkPHOC.js → index-BDAqXmpQ.js} +4 -4
  9. package/dist/web/assets/keybindings-store-L7UlPjK0.js +1 -0
  10. package/dist/web/assets/{markdown-renderer-C19IsITh.js → markdown-renderer-CYs_lrjt.js} +1 -1
  11. package/dist/web/assets/{port-forwarding-tab-BF79F1iL.js → port-forwarding-tab-CSHJ7gxM.js} +1 -1
  12. package/dist/web/assets/{postgres-viewer-_nYiO_wp.js → postgres-viewer-DlCLiEGU.js} +1 -1
  13. package/dist/web/assets/{settings-tab-C1SQMbSu.js → settings-tab-Dcr7lDcJ.js} +1 -1
  14. package/dist/web/assets/{sql-query-editor-6OFvxxuN.js → sql-query-editor-Dxx-QZaI.js} +1 -1
  15. package/dist/web/assets/{sqlite-viewer-SNVYFXvB.js → sqlite-viewer-B0052fC-.js} +1 -1
  16. package/dist/web/assets/{terminal-tab-BJEkmrDt.js → terminal-tab-yfmxGQ5e.js} +1 -1
  17. package/dist/web/assets/{use-monaco-theme-r8FzlCWr.js → use-monaco-theme-7qyyJae5.js} +1 -1
  18. package/dist/web/index.html +1 -1
  19. package/dist/web/sw.js +1 -1
  20. package/package.json +1 -1
  21. package/src/cli/commands/cloud.ts +1 -0
  22. package/src/providers/claude-agent-sdk.ts +116 -11
  23. package/src/server/routes/port-forwarding.ts +118 -67
  24. package/src/services/supervisor.ts +8 -1
  25. package/src/web/hooks/use-url-sync.ts +6 -4
  26. package/.opencode/.env.example +0 -98
  27. package/.opencode/skills/ads-management/scripts/.env.example +0 -13
  28. package/.opencode/skills/ai-multimodal/.env.example +0 -230
  29. package/.opencode/skills/cip-design/.env.example +0 -6
  30. package/.opencode/skills/devops/.env.example +0 -76
  31. package/.opencode/skills/docs-seeker/.env.example +0 -15
  32. package/.opencode/skills/elevenlabs/.env.example +0 -3
  33. package/.opencode/skills/marketing-dashboard/.env.example +0 -15
  34. package/.opencode/skills/marketing-dashboard/app/.env.example +0 -2
  35. package/.opencode/skills/marketing-dashboard/server/.env.example +0 -2
  36. package/.opencode/skills/mcp-management/scripts/dist/analyze-tools.js +0 -70
  37. package/.opencode/skills/mcp-management/scripts/dist/cli.js +0 -160
  38. package/.opencode/skills/mcp-management/scripts/dist/mcp-client.js +0 -183
  39. package/.opencode/skills/payment-integration/scripts/.env.example +0 -20
  40. package/.opencode/skills/sequential-thinking/.env.example +0 -8
  41. package/dist/web/assets/keybindings-store-DsaANvBz.js +0 -1
@@ -20,7 +20,7 @@ import { getSessionProjectPath, setSessionMetadata, getSessionTitles } from "../
20
20
  import { accountSelector } from "../services/account-selector.service.ts";
21
21
  import { accountService, type AccountWithTokens } from "../services/account.service.ts";
22
22
  import { resolve } from "node:path";
23
- import { existsSync, readdirSync, unlinkSync } from "node:fs";
23
+ import { existsSync, readdirSync, unlinkSync, readFileSync, statSync } from "node:fs";
24
24
  import { homedir } from "node:os";
25
25
 
26
26
  const CLAUDE_PROJECTS_DIR = resolve(homedir(), ".claude/projects");
@@ -322,13 +322,30 @@ export class ClaudeAgentSdkProvider implements AIProvider {
322
322
  // Overlay DB titles (user-set) over SDK titles
323
323
  const ids = sdkSessions.map((s) => s.sessionId);
324
324
  const dbTitles = getSessionTitles(ids);
325
- return sdkSessions.map((s) => ({
325
+ const sessions: SessionInfo[] = sdkSessions.map((s) => ({
326
326
  id: s.sessionId,
327
327
  providerId: this.id,
328
328
  title: dbTitles[s.sessionId] ?? s.customTitle ?? s.summary ?? s.firstPrompt ?? "Chat",
329
329
  createdAt: new Date(s.lastModified).toISOString(),
330
330
  updatedAt: new Date(s.lastModified).toISOString(),
331
331
  }));
332
+
333
+ // SDK's listSessions drops sessions whose first user message exceeds its
334
+ // 64KB head buffer (e.g. large pasted docs). Scan JSONL dir to recover them.
335
+ if (dir && offset === 0) {
336
+ const knownIds = new Set(sessions.map((s) => s.id));
337
+ const missing = findMissingSessions(dir, knownIds, this.id);
338
+ if (missing.length > 0) {
339
+ const missingIds = missing.map((s) => s.id);
340
+ const missingDbTitles = getSessionTitles(missingIds);
341
+ for (const s of missing) {
342
+ s.title = missingDbTitles[s.id] ?? s.title;
343
+ }
344
+ sessions.push(...missing);
345
+ }
346
+ }
347
+
348
+ return sessions;
332
349
  } catch {
333
350
  return Array.from(this.activeSessions.values()).map((s) => ({
334
351
  id: s.id,
@@ -343,15 +360,28 @@ export class ClaudeAgentSdkProvider implements AIProvider {
343
360
  async getSessionInfoById(sessionId: string, dir?: string): Promise<SessionInfo | null> {
344
361
  try {
345
362
  const info = await sdkGetSessionInfo(sessionId, { dir });
346
- if (!info) return null;
347
- const dbTitles = getSessionTitles([info.sessionId]);
348
- return {
349
- id: info.sessionId,
350
- providerId: this.id,
351
- title: dbTitles[info.sessionId] ?? info.customTitle ?? info.summary ?? info.firstPrompt ?? "Chat",
352
- createdAt: new Date(info.lastModified).toISOString(),
353
- updatedAt: new Date(info.lastModified).toISOString(),
354
- };
363
+ if (info) {
364
+ const dbTitles = getSessionTitles([info.sessionId]);
365
+ return {
366
+ id: info.sessionId,
367
+ providerId: this.id,
368
+ title: dbTitles[info.sessionId] ?? info.customTitle ?? info.summary ?? info.firstPrompt ?? "Chat",
369
+ createdAt: new Date(info.lastModified).toISOString(),
370
+ updatedAt: new Date(info.lastModified).toISOString(),
371
+ };
372
+ }
373
+ // SDK can't find the session (large first message, active session, etc.)
374
+ // — try direct JSONL lookup
375
+ if (dir) {
376
+ const missing = findMissingSessions(dir, new Set(), this.id);
377
+ const match = missing.find((s) => s.id === sessionId);
378
+ if (match) {
379
+ const dbTitles = getSessionTitles([sessionId]);
380
+ match.title = dbTitles[sessionId] ?? match.title;
381
+ return match;
382
+ }
383
+ }
384
+ return null;
355
385
  } catch {
356
386
  return null;
357
387
  }
@@ -1633,6 +1663,81 @@ function extractText(message: unknown): string {
1633
1663
  return "";
1634
1664
  }
1635
1665
 
1666
+ /**
1667
+ * Scan a JSONL project directory for sessions that the SDK's listSessions missed.
1668
+ * The SDK uses a 64KB head buffer; sessions with very large first messages
1669
+ * (e.g., pasted API docs) can't be parsed and are silently dropped.
1670
+ * We extract title from queue-operation content or first user message.
1671
+ */
1672
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
1673
+ function findMissingSessions(
1674
+ dir: string,
1675
+ knownIds: Set<string>,
1676
+ providerId: string,
1677
+ ): SessionInfo[] {
1678
+ let resolvedDir: string;
1679
+ try { resolvedDir = require("node:fs").realpathSync(dir).normalize("NFC"); } catch { resolvedDir = dir; }
1680
+ // Match SDK's path encoding: replace all non-alphanumeric chars with "-"
1681
+ const encodedDir = resolvedDir.replace(/[^a-zA-Z0-9]/g, "-");
1682
+ let jsonlDir = resolve(CLAUDE_PROJECTS_DIR, encodedDir);
1683
+ // SDK truncates to 200 chars + hash for long paths; try prefix match as fallback
1684
+ if (!existsSync(jsonlDir) && encodedDir.length > 200) {
1685
+ const prefix = encodedDir.slice(0, 200);
1686
+ try {
1687
+ const match = readdirSync(CLAUDE_PROJECTS_DIR).find((d) => d.startsWith(prefix + "-"));
1688
+ if (match) jsonlDir = resolve(CLAUDE_PROJECTS_DIR, match);
1689
+ else return [];
1690
+ } catch { return []; }
1691
+ }
1692
+ if (!existsSync(jsonlDir)) return [];
1693
+
1694
+ const results: SessionInfo[] = [];
1695
+ for (const file of readdirSync(jsonlDir)) {
1696
+ if (!file.endsWith(".jsonl")) continue;
1697
+ const id = file.slice(0, -6);
1698
+ if (!UUID_RE.test(id) || knownIds.has(id)) continue;
1699
+
1700
+ try {
1701
+ const filePath = resolve(jsonlDir, file);
1702
+ const stat = statSync(filePath);
1703
+ // Read first 512 bytes — enough to extract title from queue-operation content
1704
+ const buf = Buffer.alloc(512);
1705
+ const { openSync, readSync, closeSync } = require("node:fs") as typeof import("node:fs");
1706
+ const fd = openSync(filePath, "r");
1707
+ const bytesRead = readSync(fd, buf, 0, 512, 0);
1708
+ closeSync(fd);
1709
+ const head = buf.toString("utf-8", 0, bytesRead);
1710
+
1711
+ let title = "Chat";
1712
+ // Extract "content" value via string search (JSON may be truncated)
1713
+ const idx = head.indexOf('"content":"');
1714
+ if (idx >= 0) {
1715
+ const start = idx + 11; // length of '"content":"'
1716
+ // Read up to 120 chars or next unescaped quote
1717
+ let end = start;
1718
+ while (end < head.length && end - start < 200) {
1719
+ if (head[end] === "\\" ) { end += 2; continue; }
1720
+ if (head[end] === '"') break;
1721
+ end++;
1722
+ }
1723
+ const raw = head.slice(start, end).replace(/\\n/g, " ").replace(/\\"/g, '"').trim();
1724
+ if (raw.length > 0) {
1725
+ title = raw.length > 120 ? raw.slice(0, 120) + "…" : raw;
1726
+ }
1727
+ }
1728
+
1729
+ results.push({
1730
+ id,
1731
+ providerId,
1732
+ title,
1733
+ createdAt: new Date(stat.mtime).toISOString(),
1734
+ updatedAt: new Date(stat.mtime).toISOString(),
1735
+ });
1736
+ } catch { /* skip unreadable files */ }
1737
+ }
1738
+ return results;
1739
+ }
1740
+
1636
1741
  /** Strip SDK teammate-message XML tags from assistant text */
1637
1742
  const TEAMMATE_MSG_RE = /<teammate-message[^>]*>[\s\S]*?<\/teammate-message>/g;
1638
1743
  function stripTeammateXml(text: string): string {
@@ -19,11 +19,67 @@ interface ActiveTunnel {
19
19
  url: string;
20
20
  process: import("bun").Subprocess;
21
21
  startedAt: number;
22
+ probeFailures: number;
22
23
  }
23
24
 
25
+ const MAX_PROBE_FAILURES = 2;
26
+
24
27
  /** Active tunnels keyed by port — exported for testing */
25
28
  export const activeTunnels = new Map<number, ActiveTunnel>();
26
29
 
30
+ /** Spawn cloudflared tunnel for a port, extract URL from stderr */
31
+ async function spawnTunnelProcess(port: number): Promise<{ process: import("bun").Subprocess; url: string }> {
32
+ const bin = await ensureCloudflared();
33
+ const proc = Bun.spawn(
34
+ [bin, "tunnel", "--url", `http://127.0.0.1:${port}`],
35
+ { stderr: "pipe", stdout: "ignore", stdin: "ignore" },
36
+ );
37
+
38
+ const reader = proc.stderr.getReader();
39
+ const decoder = new TextDecoder();
40
+ const url = await new Promise<string>((resolve, reject) => {
41
+ const timeout = setTimeout(() => {
42
+ try { proc.kill(); } catch {}
43
+ reject(new Error("Tunnel timed out after 30s"));
44
+ }, 30_000);
45
+
46
+ let buffer = "";
47
+ let found = false;
48
+ const read = async () => {
49
+ try {
50
+ while (true) {
51
+ const { done, value } = await reader.read();
52
+ if (done) break;
53
+ if (found) continue;
54
+ buffer += decoder.decode(value, { stream: true });
55
+ const match = buffer.match(TUNNEL_URL_REGEX);
56
+ if (match) {
57
+ found = true;
58
+ buffer = "";
59
+ clearTimeout(timeout);
60
+ resolve(match[0]);
61
+ }
62
+ }
63
+ if (!found) {
64
+ clearTimeout(timeout);
65
+ reject(new Error("cloudflared exited without tunnel URL"));
66
+ }
67
+ } catch (e) {
68
+ if (!found) { clearTimeout(timeout); reject(e); }
69
+ }
70
+ };
71
+ read();
72
+ });
73
+
74
+ return { process: proc, url };
75
+ }
76
+
77
+ /** Register tunnel in map with auto-cleanup on exit */
78
+ function registerTunnel(port: number, proc: import("bun").Subprocess, url: string) {
79
+ activeTunnels.set(port, { port, url, process: proc, startedAt: Date.now(), probeFailures: 0 });
80
+ proc.exited.then(() => activeTunnels.delete(port)).catch(() => activeTunnels.delete(port));
81
+ }
82
+
27
83
  /** Start a tunnel for a localhost port */
28
84
  portForwardingRoutes.post("/tunnel", async (c) => {
29
85
  const body = await c.req.json<{ port: number }>().catch(() => null);
@@ -39,54 +95,8 @@ portForwardingRoutes.post("/tunnel", async (c) => {
39
95
  }
40
96
 
41
97
  try {
42
- const bin = await ensureCloudflared();
43
- const proc = Bun.spawn(
44
- [bin, "tunnel", "--url", `http://127.0.0.1:${port}`],
45
- { stderr: "pipe", stdout: "ignore", stdin: "ignore" },
46
- );
47
-
48
- // Read stderr to find tunnel URL
49
- const reader = proc.stderr.getReader();
50
- const decoder = new TextDecoder();
51
- const url = await new Promise<string>((resolve, reject) => {
52
- const timeout = setTimeout(() => {
53
- try { proc.kill(); } catch {}
54
- reject(new Error("Tunnel timed out after 30s"));
55
- }, 30_000);
56
-
57
- let buffer = "";
58
- let found = false;
59
- const read = async () => {
60
- try {
61
- while (true) {
62
- const { done, value } = await reader.read();
63
- if (done) break;
64
- if (found) continue;
65
- buffer += decoder.decode(value, { stream: true });
66
- const match = buffer.match(TUNNEL_URL_REGEX);
67
- if (match) {
68
- found = true;
69
- buffer = "";
70
- clearTimeout(timeout);
71
- resolve(match[0]);
72
- }
73
- }
74
- if (!found) {
75
- clearTimeout(timeout);
76
- reject(new Error("cloudflared exited without tunnel URL"));
77
- }
78
- } catch (e) {
79
- if (!found) { clearTimeout(timeout); reject(e); }
80
- }
81
- };
82
- read();
83
- });
84
-
85
- activeTunnels.set(port, { port, url, process: proc, startedAt: Date.now() });
86
-
87
- // Auto-cleanup when process exits
88
- proc.exited.then(() => activeTunnels.delete(port)).catch(() => activeTunnels.delete(port));
89
-
98
+ const { process: proc, url } = await spawnTunnelProcess(port);
99
+ registerTunnel(port, proc, url);
90
100
  console.log(`[preview] tunnel started for port ${port} → ${url}`);
91
101
  return c.json(ok({ port, url }));
92
102
  } catch (e: any) {
@@ -123,27 +133,68 @@ function isProcessAlive(proc: import("bun").Subprocess): boolean {
123
133
  try { process.kill(proc.pid, 0); return true; } catch { return false; }
124
134
  }
125
135
 
126
- /** Remove ghost tunnels (process died or target port no longer listening) */
136
+ /** Probe tunnel URL to check if it's still accessible (DNS + connection) */
137
+ async function probeTunnelUrl(url: string): Promise<boolean> {
138
+ try {
139
+ await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(8_000), redirect: "follow" });
140
+ return true;
141
+ } catch {
142
+ return false;
143
+ }
144
+ }
145
+
146
+ let cleanupRunning = false;
147
+
148
+ /** Remove ghost tunnels and auto-restart tunnels with expired URLs */
127
149
  async function cleanupGhostTunnels() {
128
- for (const [port, tunnel] of activeTunnels) {
129
- // Check if cloudflared process is still running
130
- if (!isProcessAlive(tunnel.process)) {
131
- console.log(`[preview] ghost cleanup: tunnel for port ${port} — process dead`);
132
- activeTunnels.delete(port);
133
- continue;
134
- }
135
- // Check if target port is still listening
136
- try {
137
- const conn = await Bun.connect({ hostname: "127.0.0.1", port, socket: {
138
- data() {}, open(s) { s.end(); }, error() {}, close() {},
139
- }});
140
- conn.end();
141
- } catch {
142
- // Port not listening — kill tunnel
143
- console.log(`[preview] ghost cleanup: tunnel for port ${port} — port not listening`);
144
- try { tunnel.process.kill(); } catch {}
145
- activeTunnels.delete(port);
150
+ if (cleanupRunning) return;
151
+ cleanupRunning = true;
152
+ try {
153
+ for (const [port, tunnel] of activeTunnels) {
154
+ // Check if cloudflared process is still running
155
+ if (!isProcessAlive(tunnel.process)) {
156
+ console.log(`[preview] ghost cleanup: port ${port} — process dead`);
157
+ activeTunnels.delete(port);
158
+ continue;
159
+ }
160
+ // Check if target port is still listening
161
+ try {
162
+ const conn = await Bun.connect({ hostname: "127.0.0.1", port, socket: {
163
+ data() {}, open(s) { s.end(); }, error() {}, close() {},
164
+ }});
165
+ conn.end();
166
+ } catch {
167
+ console.log(`[preview] ghost cleanup: port ${port} — port not listening`);
168
+ try { tunnel.process.kill(); } catch {}
169
+ activeTunnels.delete(port);
170
+ continue;
171
+ }
172
+
173
+ // Probe tunnel URL to detect expired Quick Tunnel URLs
174
+ const alive = await probeTunnelUrl(tunnel.url);
175
+ if (alive) {
176
+ tunnel.probeFailures = 0;
177
+ continue;
178
+ }
179
+
180
+ tunnel.probeFailures++;
181
+ console.log(`[preview] tunnel probe failed for port ${port} (${tunnel.probeFailures}/${MAX_PROBE_FAILURES})`);
182
+
183
+ if (tunnel.probeFailures >= MAX_PROBE_FAILURES) {
184
+ console.log(`[preview] tunnel URL expired for port ${port}, restarting...`);
185
+ try { tunnel.process.kill(); } catch {}
186
+ activeTunnels.delete(port);
187
+ try {
188
+ const { process: proc, url } = await spawnTunnelProcess(port);
189
+ registerTunnel(port, proc, url);
190
+ console.log(`[preview] tunnel restarted for port ${port} → ${url}`);
191
+ } catch (e: any) {
192
+ console.warn(`[preview] tunnel restart failed for port ${port}: ${e.message}`);
193
+ }
194
+ }
146
195
  }
196
+ } finally {
197
+ cleanupRunning = false;
147
198
  }
148
199
  }
149
200
 
@@ -504,7 +504,8 @@ async function notifyStateChange(from: string, to: string, reason: string) {
504
504
  /** Connect supervisor to Cloud via WebSocket (if device is linked) */
505
505
  async function connectCloud(opts: { port: number }, serverArgs: string[], logFd: number): Promise<boolean> {
506
506
  try {
507
- const { getCloudDevice } = await import("./cloud.service.ts");
507
+ const { getCloudDevice, saveCloudDevice } = await import("./cloud.service.ts");
508
+ const { configService } = await import("./config.service.ts");
508
509
  const device = getCloudDevice();
509
510
  if (!device) return false; // not linked to cloud
510
511
 
@@ -520,6 +521,12 @@ async function connectCloud(opts: { port: number }, serverArgs: string[], logFd:
520
521
  const status = readStatus();
521
522
  // Re-read device file each heartbeat to pick up name changes
522
523
  const currentDevice = getCloudDevice();
524
+ // Sync device name from config if user changed it in settings
525
+ const configName = configService.get("device_name") as string;
526
+ if (configName && currentDevice && configName !== currentDevice.name) {
527
+ currentDevice.name = configName;
528
+ saveCloudDevice(currentDevice);
529
+ }
523
530
  return {
524
531
  type: "heartbeat" as const,
525
532
  tunnelUrl,
@@ -100,10 +100,12 @@ function buildMetadataFromUrl(
100
100
  case "chat": {
101
101
  if (!identifier) return null;
102
102
  const slashIdx = identifier.indexOf("/");
103
- if (slashIdx === -1) return { sessionId: identifier, projectName };
104
- const providerId = identifier.slice(0, slashIdx);
105
- const sessionId = identifier.slice(slashIdx + 1);
106
- return sessionId ? { sessionId, providerId, projectName } : null;
103
+ const rawSessionId = slashIdx === -1 ? identifier : identifier.slice(slashIdx + 1);
104
+ const providerId = slashIdx === -1 ? undefined : identifier.slice(0, slashIdx);
105
+ // Validate UUID format short/random IDs from tab derivation must not be used as session IDs
106
+ const isUuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(rawSessionId);
107
+ const sessionId = isUuid ? rawSessionId : undefined;
108
+ return { ...(sessionId && { sessionId }), ...(providerId && { providerId }), projectName };
107
109
  }
108
110
  case "terminal": return { terminalIndex: parseInt(identifier ?? "1", 10), projectName };
109
111
  case "git-graph": return { projectName };
@@ -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