thepopebot 1.2.72-beta.21 → 1.2.72-beta.22
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/lib/chat/actions.js +15 -0
- package/lib/chat/components/app-sidebar.js +16 -15
- package/lib/chat/components/app-sidebar.jsx +19 -15
- package/lib/chat/components/chats-page.js +18 -4
- package/lib/chat/components/chats-page.jsx +14 -6
- package/lib/chat/components/sidebar-history-item.js +3 -1
- package/lib/chat/components/sidebar-history-item.jsx +3 -1
- package/lib/chat/components/sidebar-user-nav.js +4 -6
- package/lib/chat/components/sidebar-user-nav.jsx +10 -6
- package/lib/code/ws-proxy.js +7 -0
- package/package.json +1 -1
- package/templates/app/code/[claudeWorkspaceId]/page.js +4 -0
- package/templates/skills/google-docs/SKILL.md +23 -0
- package/templates/skills/google-docs/create.sh +69 -0
- package/templates/skills/google-drive/SKILL.md +47 -0
- package/templates/skills/google-drive/delete.sh +47 -0
- package/templates/skills/google-drive/download.sh +50 -0
- package/templates/skills/google-drive/list.sh +41 -0
- package/templates/skills/google-drive/upload.sh +76 -0
- package/templates/skills/kie-ai/SKILL.md +38 -0
- package/templates/skills/kie-ai/generate-image.sh +77 -0
- package/templates/skills/kie-ai/generate-video.sh +69 -0
- package/templates/skills/youtube-transcript/SKILL.md +41 -0
- package/templates/skills/youtube-transcript/package-lock.json +24 -0
- package/templates/skills/youtube-transcript/package.json +8 -0
- package/templates/skills/youtube-transcript/transcript.js +84 -0
package/lib/chat/actions.js
CHANGED
|
@@ -202,6 +202,21 @@ export async function triggerUpgrade() {
|
|
|
202
202
|
return { success: true };
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
206
|
+
// Feature flags
|
|
207
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Get feature flags based on environment configuration.
|
|
211
|
+
* Features gated behind CLAUDE_CODE_OAUTH_TOKEN (Claude Pro/Max subscription features).
|
|
212
|
+
* @returns {Promise<{ claudeWorkspace: boolean }>}
|
|
213
|
+
*/
|
|
214
|
+
export async function getFeatureFlags() {
|
|
215
|
+
await requireAuth();
|
|
216
|
+
const claudeWorkspace = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
217
|
+
return { claudeWorkspace };
|
|
218
|
+
}
|
|
219
|
+
|
|
205
220
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
206
221
|
// API Key actions
|
|
207
222
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState, useEffect } from "react";
|
|
4
4
|
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ArrowUpCircleIcon, LifeBuoyIcon, GitPullRequestIcon } from "./icons.js";
|
|
5
|
-
import { getUnreadNotificationCount, getPullRequestCount, getAppVersion } from "../actions.js";
|
|
5
|
+
import { getUnreadNotificationCount, getPullRequestCount, getAppVersion, getFeatureFlags } from "../actions.js";
|
|
6
6
|
import { SidebarHistory } from "./sidebar-history.js";
|
|
7
7
|
import { SidebarUserNav } from "./sidebar-user-nav.js";
|
|
8
8
|
import { UpgradeDialog } from "./upgrade-dialog.js";
|
|
@@ -30,6 +30,7 @@ function AppSidebar({ user }) {
|
|
|
30
30
|
const [updateAvailable, setUpdateAvailable] = useState(null);
|
|
31
31
|
const [changelog, setChangelog] = useState(null);
|
|
32
32
|
const [upgradeOpen, setUpgradeOpen] = useState(false);
|
|
33
|
+
const [features, setFeatures] = useState({ claudeWorkspace: false });
|
|
33
34
|
useEffect(() => {
|
|
34
35
|
function fetchCounts() {
|
|
35
36
|
getUnreadNotificationCount().then((count) => setUnreadCount(count)).catch(() => {
|
|
@@ -41,6 +42,10 @@ function AppSidebar({ user }) {
|
|
|
41
42
|
const interval = setInterval(fetchCounts, 10 * 60 * 1e3);
|
|
42
43
|
return () => clearInterval(interval);
|
|
43
44
|
}, []);
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
getFeatureFlags().then((flags) => setFeatures(flags)).catch(() => {
|
|
47
|
+
});
|
|
48
|
+
}, []);
|
|
44
49
|
useEffect(() => {
|
|
45
50
|
getAppVersion().then(({ version: version2, updateAvailable: updateAvailable2, changelog: changelog2 }) => {
|
|
46
51
|
setVersion(version2);
|
|
@@ -77,8 +82,10 @@ function AppSidebar({ user }) {
|
|
|
77
82
|
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
78
83
|
SidebarMenuButton,
|
|
79
84
|
{
|
|
85
|
+
href: "/",
|
|
80
86
|
className: collapsed ? "justify-center" : "",
|
|
81
|
-
onClick: () => {
|
|
87
|
+
onClick: (e) => {
|
|
88
|
+
e.preventDefault();
|
|
82
89
|
navigateToChat(null);
|
|
83
90
|
setOpenMobile(false);
|
|
84
91
|
},
|
|
@@ -94,10 +101,8 @@ function AppSidebar({ user }) {
|
|
|
94
101
|
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
95
102
|
SidebarMenuButton,
|
|
96
103
|
{
|
|
104
|
+
href: "/chats",
|
|
97
105
|
className: collapsed ? "justify-center" : "",
|
|
98
|
-
onClick: () => {
|
|
99
|
-
window.location.href = "/chats";
|
|
100
|
-
},
|
|
101
106
|
children: [
|
|
102
107
|
/* @__PURE__ */ jsx(MessageIcon, { size: 16 }),
|
|
103
108
|
!collapsed && /* @__PURE__ */ jsx("span", { children: "Chats" })
|
|
@@ -110,10 +115,8 @@ function AppSidebar({ user }) {
|
|
|
110
115
|
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
111
116
|
SidebarMenuButton,
|
|
112
117
|
{
|
|
118
|
+
href: "/swarm",
|
|
113
119
|
className: collapsed ? "justify-center" : "",
|
|
114
|
-
onClick: () => {
|
|
115
|
-
window.location.href = "/swarm";
|
|
116
|
-
},
|
|
117
120
|
children: [
|
|
118
121
|
/* @__PURE__ */ jsx(SwarmIcon, { size: 16 }),
|
|
119
122
|
!collapsed && /* @__PURE__ */ jsx("span", { children: "Swarm" })
|
|
@@ -126,10 +129,8 @@ function AppSidebar({ user }) {
|
|
|
126
129
|
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
127
130
|
SidebarMenuButton,
|
|
128
131
|
{
|
|
132
|
+
href: "/notifications",
|
|
129
133
|
className: collapsed ? "justify-center" : "",
|
|
130
|
-
onClick: () => {
|
|
131
|
-
window.location.href = "/notifications";
|
|
132
|
-
},
|
|
133
134
|
children: [
|
|
134
135
|
/* @__PURE__ */ jsx(BellIcon, { size: 16 }),
|
|
135
136
|
!collapsed && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
@@ -146,10 +147,8 @@ function AppSidebar({ user }) {
|
|
|
146
147
|
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
147
148
|
SidebarMenuButton,
|
|
148
149
|
{
|
|
150
|
+
href: "/pull-requests",
|
|
149
151
|
className: collapsed ? "justify-center" : "",
|
|
150
|
-
onClick: () => {
|
|
151
|
-
window.location.href = "/pull-requests";
|
|
152
|
-
},
|
|
153
152
|
children: [
|
|
154
153
|
/* @__PURE__ */ jsx(GitPullRequestIcon, { size: 16 }),
|
|
155
154
|
!collapsed && /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-2", children: [
|
|
@@ -192,8 +191,10 @@ function AppSidebar({ user }) {
|
|
|
192
191
|
/* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
|
|
193
192
|
SidebarMenuButton,
|
|
194
193
|
{
|
|
194
|
+
href: "https://www.skool.com/ai-architects",
|
|
195
|
+
target: "_blank",
|
|
196
|
+
rel: "noopener noreferrer",
|
|
195
197
|
className: collapsed ? "justify-center" : "",
|
|
196
|
-
onClick: () => window.open("https://www.skool.com/ai-architects", "_blank"),
|
|
197
198
|
children: [
|
|
198
199
|
/* @__PURE__ */ jsx(LifeBuoyIcon, { size: 16 }),
|
|
199
200
|
!collapsed && /* @__PURE__ */ jsx("span", { children: "Support" })
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useState, useEffect } from 'react';
|
|
4
4
|
import { CirclePlusIcon, PanelLeftIcon, MessageIcon, BellIcon, SwarmIcon, ArrowUpCircleIcon, LifeBuoyIcon, GitPullRequestIcon } from './icons.js';
|
|
5
|
-
import { getUnreadNotificationCount, getPullRequestCount, getAppVersion } from '../actions.js';
|
|
5
|
+
import { getUnreadNotificationCount, getPullRequestCount, getAppVersion, getFeatureFlags } from '../actions.js';
|
|
6
6
|
import { SidebarHistory } from './sidebar-history.js';
|
|
7
7
|
import { SidebarUserNav } from './sidebar-user-nav.js';
|
|
8
8
|
import { UpgradeDialog } from './upgrade-dialog.js';
|
|
@@ -31,6 +31,7 @@ export function AppSidebar({ user }) {
|
|
|
31
31
|
const [updateAvailable, setUpdateAvailable] = useState(null);
|
|
32
32
|
const [changelog, setChangelog] = useState(null);
|
|
33
33
|
const [upgradeOpen, setUpgradeOpen] = useState(false);
|
|
34
|
+
const [features, setFeatures] = useState({ claudeWorkspace: false });
|
|
34
35
|
|
|
35
36
|
// Fetch badge counts (notifications + PRs) — run immediately, then every 10 minutes
|
|
36
37
|
useEffect(() => {
|
|
@@ -47,6 +48,13 @@ export function AppSidebar({ user }) {
|
|
|
47
48
|
return () => clearInterval(interval);
|
|
48
49
|
}, []);
|
|
49
50
|
|
|
51
|
+
// Feature flags — one-time on mount
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
getFeatureFlags()
|
|
54
|
+
.then((flags) => setFeatures(flags))
|
|
55
|
+
.catch(() => {});
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
50
58
|
// Version check — one-time on mount
|
|
51
59
|
useEffect(() => {
|
|
52
60
|
getAppVersion()
|
|
@@ -88,8 +96,10 @@ export function AppSidebar({ user }) {
|
|
|
88
96
|
<Tooltip>
|
|
89
97
|
<TooltipTrigger asChild>
|
|
90
98
|
<SidebarMenuButton
|
|
99
|
+
href="/"
|
|
91
100
|
className={collapsed ? 'justify-center' : ''}
|
|
92
|
-
onClick={() => {
|
|
101
|
+
onClick={(e) => {
|
|
102
|
+
e.preventDefault();
|
|
93
103
|
navigateToChat(null);
|
|
94
104
|
setOpenMobile(false);
|
|
95
105
|
}}
|
|
@@ -109,10 +119,8 @@ export function AppSidebar({ user }) {
|
|
|
109
119
|
<Tooltip>
|
|
110
120
|
<TooltipTrigger asChild>
|
|
111
121
|
<SidebarMenuButton
|
|
122
|
+
href="/chats"
|
|
112
123
|
className={collapsed ? 'justify-center' : ''}
|
|
113
|
-
onClick={() => {
|
|
114
|
-
window.location.href = '/chats';
|
|
115
|
-
}}
|
|
116
124
|
>
|
|
117
125
|
<MessageIcon size={16} />
|
|
118
126
|
{!collapsed && <span>Chats</span>}
|
|
@@ -129,10 +137,8 @@ export function AppSidebar({ user }) {
|
|
|
129
137
|
<Tooltip>
|
|
130
138
|
<TooltipTrigger asChild>
|
|
131
139
|
<SidebarMenuButton
|
|
140
|
+
href="/swarm"
|
|
132
141
|
className={collapsed ? 'justify-center' : ''}
|
|
133
|
-
onClick={() => {
|
|
134
|
-
window.location.href = '/swarm';
|
|
135
|
-
}}
|
|
136
142
|
>
|
|
137
143
|
<SwarmIcon size={16} />
|
|
138
144
|
{!collapsed && <span>Swarm</span>}
|
|
@@ -149,10 +155,8 @@ export function AppSidebar({ user }) {
|
|
|
149
155
|
<Tooltip>
|
|
150
156
|
<TooltipTrigger asChild>
|
|
151
157
|
<SidebarMenuButton
|
|
158
|
+
href="/notifications"
|
|
152
159
|
className={collapsed ? 'justify-center' : ''}
|
|
153
|
-
onClick={() => {
|
|
154
|
-
window.location.href = '/notifications';
|
|
155
|
-
}}
|
|
156
160
|
>
|
|
157
161
|
<BellIcon size={16} />
|
|
158
162
|
{!collapsed && (
|
|
@@ -183,10 +187,8 @@ export function AppSidebar({ user }) {
|
|
|
183
187
|
<Tooltip>
|
|
184
188
|
<TooltipTrigger asChild>
|
|
185
189
|
<SidebarMenuButton
|
|
190
|
+
href="/pull-requests"
|
|
186
191
|
className={collapsed ? 'justify-center' : ''}
|
|
187
|
-
onClick={() => {
|
|
188
|
-
window.location.href = '/pull-requests';
|
|
189
|
-
}}
|
|
190
192
|
>
|
|
191
193
|
<GitPullRequestIcon size={16} />
|
|
192
194
|
{!collapsed && (
|
|
@@ -249,8 +251,10 @@ export function AppSidebar({ user }) {
|
|
|
249
251
|
<Tooltip>
|
|
250
252
|
<TooltipTrigger asChild>
|
|
251
253
|
<SidebarMenuButton
|
|
254
|
+
href="https://www.skool.com/ai-architects"
|
|
255
|
+
target="_blank"
|
|
256
|
+
rel="noopener noreferrer"
|
|
252
257
|
className={collapsed ? 'justify-center' : ''}
|
|
253
|
-
onClick={() => window.open('https://www.skool.com/ai-architects', '_blank')}
|
|
254
258
|
>
|
|
255
259
|
<LifeBuoyIcon size={16} />
|
|
256
260
|
{!collapsed && <span>Support</span>}
|
|
@@ -103,10 +103,15 @@ function ChatsPage({ session }) {
|
|
|
103
103
|
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between mb-6", children: [
|
|
104
104
|
/* @__PURE__ */ jsx("h1", { className: "text-2xl font-semibold", children: "Chats" }),
|
|
105
105
|
/* @__PURE__ */ jsxs(
|
|
106
|
-
"
|
|
106
|
+
"a",
|
|
107
107
|
{
|
|
108
|
-
|
|
108
|
+
href: "/",
|
|
109
|
+
onClick: (e) => {
|
|
110
|
+
e.preventDefault();
|
|
111
|
+
navigateToChat(null);
|
|
112
|
+
},
|
|
109
113
|
className: "inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium bg-foreground text-background hover:bg-foreground/90",
|
|
114
|
+
style: { textDecoration: "inherit" },
|
|
110
115
|
children: [
|
|
111
116
|
/* @__PURE__ */ jsx(PlusIcon, { size: 14 }),
|
|
112
117
|
"New chat"
|
|
@@ -180,12 +185,21 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
180
185
|
setEditTitle(chat.title || "");
|
|
181
186
|
};
|
|
182
187
|
return /* @__PURE__ */ jsxs(
|
|
183
|
-
"
|
|
188
|
+
"a",
|
|
184
189
|
{
|
|
190
|
+
href: `/chat/${chat.id}`,
|
|
185
191
|
className: "relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md",
|
|
192
|
+
style: { textDecoration: "inherit", color: "inherit" },
|
|
186
193
|
onMouseEnter: () => setHovered(true),
|
|
187
194
|
onMouseLeave: () => setHovered(false),
|
|
188
|
-
onClick: () =>
|
|
195
|
+
onClick: (e) => {
|
|
196
|
+
if (editing) {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
e.preventDefault();
|
|
201
|
+
onNavigate(chat.id);
|
|
202
|
+
},
|
|
189
203
|
children: [
|
|
190
204
|
/* @__PURE__ */ jsx(MessageIcon, { size: 16 }),
|
|
191
205
|
/* @__PURE__ */ jsxs("div", { className: "flex-1 min-w-0", children: [
|
|
@@ -122,13 +122,15 @@ export function ChatsPage({ session }) {
|
|
|
122
122
|
{/* Header */}
|
|
123
123
|
<div className="flex items-center justify-between mb-6">
|
|
124
124
|
<h1 className="text-2xl font-semibold">Chats</h1>
|
|
125
|
-
<
|
|
126
|
-
|
|
125
|
+
<a
|
|
126
|
+
href="/"
|
|
127
|
+
onClick={(e) => { e.preventDefault(); navigateToChat(null); }}
|
|
127
128
|
className="inline-flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium bg-foreground text-background hover:bg-foreground/90"
|
|
129
|
+
style={{ textDecoration: 'inherit' }}
|
|
128
130
|
>
|
|
129
131
|
<PlusIcon size={14} />
|
|
130
132
|
New chat
|
|
131
|
-
</
|
|
133
|
+
</a>
|
|
132
134
|
</div>
|
|
133
135
|
|
|
134
136
|
{/* Search */}
|
|
@@ -226,11 +228,17 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
226
228
|
};
|
|
227
229
|
|
|
228
230
|
return (
|
|
229
|
-
<
|
|
231
|
+
<a
|
|
232
|
+
href={`/chat/${chat.id}`}
|
|
230
233
|
className="relative group flex items-center gap-3 px-3 py-3 cursor-pointer hover:bg-muted/50 rounded-md"
|
|
234
|
+
style={{ textDecoration: 'inherit', color: 'inherit' }}
|
|
231
235
|
onMouseEnter={() => setHovered(true)}
|
|
232
236
|
onMouseLeave={() => setHovered(false)}
|
|
233
|
-
onClick={() =>
|
|
237
|
+
onClick={(e) => {
|
|
238
|
+
if (editing) { e.preventDefault(); return; }
|
|
239
|
+
e.preventDefault();
|
|
240
|
+
onNavigate(chat.id);
|
|
241
|
+
}}
|
|
234
242
|
>
|
|
235
243
|
<MessageIcon size={16} />
|
|
236
244
|
<div className="flex-1 min-w-0">
|
|
@@ -325,6 +333,6 @@ function ChatRow({ chat, onNavigate, onDelete, onStar, onRename }) {
|
|
|
325
333
|
}}
|
|
326
334
|
onCancel={() => setConfirmDelete(false)}
|
|
327
335
|
/>
|
|
328
|
-
</
|
|
336
|
+
</a>
|
|
329
337
|
);
|
|
330
338
|
}
|
|
@@ -27,9 +27,11 @@ function SidebarHistoryItem({ chat, isActive, onDelete, onStar, onRename }) {
|
|
|
27
27
|
/* @__PURE__ */ jsxs(
|
|
28
28
|
SidebarMenuButton,
|
|
29
29
|
{
|
|
30
|
+
href: `/chat/${chat.id}`,
|
|
30
31
|
className: "pr-8",
|
|
31
32
|
isActive,
|
|
32
|
-
onClick: () => {
|
|
33
|
+
onClick: (e) => {
|
|
34
|
+
e.preventDefault();
|
|
33
35
|
navigateToChat(chat.id);
|
|
34
36
|
setOpenMobile(false);
|
|
35
37
|
},
|
|
@@ -27,9 +27,11 @@ export function SidebarHistoryItem({ chat, isActive, onDelete, onStar, onRename
|
|
|
27
27
|
onMouseLeave={() => setHovered(false)}
|
|
28
28
|
>
|
|
29
29
|
<SidebarMenuButton
|
|
30
|
+
href={`/chat/${chat.id}`}
|
|
30
31
|
className="pr-8"
|
|
31
32
|
isActive={isActive}
|
|
32
|
-
onClick={() => {
|
|
33
|
+
onClick={(e) => {
|
|
34
|
+
e.preventDefault();
|
|
33
35
|
navigateToChat(chat.id);
|
|
34
36
|
setOpenMobile(false);
|
|
35
37
|
}}
|
|
@@ -29,20 +29,18 @@ function SidebarUserNav({ user, collapsed }) {
|
|
|
29
29
|
] })
|
|
30
30
|
] }) }),
|
|
31
31
|
/* @__PURE__ */ jsxs(DropdownMenuContent, { align: "start", side: "top", className: "w-56", children: [
|
|
32
|
-
/* @__PURE__ */
|
|
33
|
-
window.location.href = "/settings";
|
|
34
|
-
}, children: [
|
|
32
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, { asChild: true, children: /* @__PURE__ */ jsxs("a", { href: "/settings", style: { textDecoration: "inherit", color: "inherit" }, children: [
|
|
35
33
|
/* @__PURE__ */ jsx(SettingsIcon, { size: 14 }),
|
|
36
34
|
/* @__PURE__ */ jsx("span", { className: "ml-2", children: "Settings" })
|
|
37
|
-
] }),
|
|
35
|
+
] }) }),
|
|
38
36
|
mounted && /* @__PURE__ */ jsxs(DropdownMenuItem, { onClick: () => setTheme(theme === "dark" ? "light" : "dark"), children: [
|
|
39
37
|
theme === "dark" ? /* @__PURE__ */ jsx(SunIcon, { size: 14 }) : /* @__PURE__ */ jsx(MoonIcon, { size: 14 }),
|
|
40
38
|
/* @__PURE__ */ jsx("span", { className: "ml-2", children: theme === "dark" ? "Light Mode" : "Dark Mode" })
|
|
41
39
|
] }),
|
|
42
|
-
/* @__PURE__ */
|
|
40
|
+
/* @__PURE__ */ jsx(DropdownMenuItem, { asChild: true, children: /* @__PURE__ */ jsxs("a", { href: "https://github.com/stephengpope/thepopebot/issues", target: "_blank", rel: "noopener noreferrer", style: { textDecoration: "inherit", color: "inherit" }, children: [
|
|
43
41
|
/* @__PURE__ */ jsx(BugIcon, { size: 14 }),
|
|
44
42
|
/* @__PURE__ */ jsx("span", { className: "ml-2", children: "Report Issues" })
|
|
45
|
-
] }),
|
|
43
|
+
] }) }),
|
|
46
44
|
/* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
47
45
|
/* @__PURE__ */ jsxs(
|
|
48
46
|
DropdownMenuItem,
|
|
@@ -43,9 +43,11 @@ export function SidebarUserNav({ user, collapsed }) {
|
|
|
43
43
|
</SidebarMenuButton>
|
|
44
44
|
</DropdownMenuTrigger>
|
|
45
45
|
<DropdownMenuContent align="start" side="top" className="w-56">
|
|
46
|
-
<DropdownMenuItem
|
|
47
|
-
<
|
|
48
|
-
|
|
46
|
+
<DropdownMenuItem asChild>
|
|
47
|
+
<a href="/settings" style={{ textDecoration: 'inherit', color: 'inherit' }}>
|
|
48
|
+
<SettingsIcon size={14} />
|
|
49
|
+
<span className="ml-2">Settings</span>
|
|
50
|
+
</a>
|
|
49
51
|
</DropdownMenuItem>
|
|
50
52
|
{mounted && (
|
|
51
53
|
<DropdownMenuItem onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}>
|
|
@@ -53,9 +55,11 @@ export function SidebarUserNav({ user, collapsed }) {
|
|
|
53
55
|
<span className="ml-2">{theme === 'dark' ? 'Light Mode' : 'Dark Mode'}</span>
|
|
54
56
|
</DropdownMenuItem>
|
|
55
57
|
)}
|
|
56
|
-
<DropdownMenuItem
|
|
57
|
-
<
|
|
58
|
-
|
|
58
|
+
<DropdownMenuItem asChild>
|
|
59
|
+
<a href="https://github.com/stephengpope/thepopebot/issues" target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'inherit', color: 'inherit' }}>
|
|
60
|
+
<BugIcon size={14} />
|
|
61
|
+
<span className="ml-2">Report Issues</span>
|
|
62
|
+
</a>
|
|
59
63
|
</DropdownMenuItem>
|
|
60
64
|
<DropdownMenuSeparator />
|
|
61
65
|
<DropdownMenuItem
|
package/lib/code/ws-proxy.js
CHANGED
|
@@ -30,6 +30,13 @@ export function attachCodeProxy(server) {
|
|
|
30
30
|
const match = req.url.match(/^\/code\/([^/]+)\/ws$/);
|
|
31
31
|
if (!match) return;
|
|
32
32
|
|
|
33
|
+
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
34
|
+
console.log('[ws-proxy] rejected: CLAUDE_CODE_OAUTH_TOKEN not set');
|
|
35
|
+
socket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
|
|
36
|
+
socket.destroy();
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
33
40
|
if (!await isAuthenticated(req)) {
|
|
34
41
|
console.log('[ws-proxy] rejected: unauthenticated upgrade');
|
|
35
42
|
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
|
package/package.json
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { auth } from 'thepopebot/auth';
|
|
2
|
+
import { redirect } from 'next/navigation';
|
|
2
3
|
import { CodePage } from 'thepopebot/code';
|
|
4
|
+
import { getFeatureFlags } from 'thepopebot/chat/actions';
|
|
3
5
|
|
|
4
6
|
export default async function CodeRoute({ params }) {
|
|
5
7
|
const session = await auth();
|
|
8
|
+
const { claudeWorkspace } = await getFeatureFlags();
|
|
9
|
+
if (!claudeWorkspace) redirect('/');
|
|
6
10
|
const { claudeWorkspaceId } = await params;
|
|
7
11
|
return <CodePage session={session} claudeWorkspaceId={claudeWorkspaceId} />;
|
|
8
12
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: google-docs
|
|
3
|
+
description: "Create and manage Google Docs on a shared drive via service account."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Google Docs
|
|
7
|
+
|
|
8
|
+
Create and manage Google Docs on a shared drive using a service account.
|
|
9
|
+
|
|
10
|
+
## Environment Variables
|
|
11
|
+
|
|
12
|
+
- `GOOGLE_SERVICE_ACCOUNT_JSON` — Service account credentials JSON
|
|
13
|
+
- `GOOGLE_SHARED_DRIVE_ID` — Shared drive ID
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### Create a Google Doc
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
skills/google-docs/create.sh <title> <content> <parent_folder_id>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Creates a new Google Doc with the given title and text content in the specified folder. Returns the document ID and URL.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
TITLE="${1:?Usage: create.sh <title> <content> <parent_folder_id>}"
|
|
5
|
+
CONTENT="${2:?Usage: create.sh <title> <content> <parent_folder_id>}"
|
|
6
|
+
PARENT_FOLDER_ID="${3:?Usage: create.sh <title> <content> <parent_folder_id>}"
|
|
7
|
+
|
|
8
|
+
if [ -z "${GOOGLE_SERVICE_ACCOUNT_JSON:-}" ]; then
|
|
9
|
+
echo "Error: GOOGLE_SERVICE_ACCOUNT_JSON is not set" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Extract service account email and private key
|
|
14
|
+
SA_EMAIL=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.client_email')
|
|
15
|
+
SA_KEY=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.private_key')
|
|
16
|
+
TOKEN_URI=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.token_uri')
|
|
17
|
+
|
|
18
|
+
# Build JWT with both drive and docs scopes
|
|
19
|
+
NOW=$(date +%s)
|
|
20
|
+
EXP=$((NOW + 3600))
|
|
21
|
+
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
22
|
+
CLAIMS=$(printf '{"iss":"%s","scope":"https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/documents","aud":"%s","iat":%d,"exp":%d}' \
|
|
23
|
+
"$SA_EMAIL" "$TOKEN_URI" "$NOW" "$EXP" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
24
|
+
SIGNING_INPUT="${HEADER}.${CLAIMS}"
|
|
25
|
+
SIGNATURE=$(printf '%s' "$SIGNING_INPUT" | openssl dgst -sha256 -sign <(echo "$SA_KEY") | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
26
|
+
JWT="${SIGNING_INPUT}.${SIGNATURE}"
|
|
27
|
+
|
|
28
|
+
# Exchange JWT for access token
|
|
29
|
+
ACCESS_TOKEN=$(curl -s -X POST "$TOKEN_URI" \
|
|
30
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
31
|
+
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${JWT}" | jq -r '.access_token')
|
|
32
|
+
|
|
33
|
+
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
|
|
34
|
+
echo "Error: Failed to obtain access token" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
# Create Google Doc via Drive API (creates empty doc in the shared drive)
|
|
39
|
+
METADATA=$(jq -n --arg title "$TITLE" --arg parent "$PARENT_FOLDER_ID" \
|
|
40
|
+
'{name: $title, mimeType: "application/vnd.google-apps.document", parents: [$parent]}')
|
|
41
|
+
|
|
42
|
+
CREATE_RESPONSE=$(curl -s -X POST \
|
|
43
|
+
"https://www.googleapis.com/drive/v3/files?supportsAllDrives=true&includeItemsFromAllDrives=true" \
|
|
44
|
+
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
45
|
+
-H "Content-Type: application/json" \
|
|
46
|
+
-d "$METADATA")
|
|
47
|
+
|
|
48
|
+
DOC_ID=$(echo "$CREATE_RESPONSE" | jq -r '.id')
|
|
49
|
+
if [ -z "$DOC_ID" ] || [ "$DOC_ID" = "null" ]; then
|
|
50
|
+
echo "Error: Failed to create document" >&2
|
|
51
|
+
echo "$CREATE_RESPONSE" >&2
|
|
52
|
+
exit 1
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Insert content using Docs API batchUpdate
|
|
56
|
+
if [ -n "$CONTENT" ]; then
|
|
57
|
+
UPDATE_BODY=$(jq -n --arg text "$CONTENT" \
|
|
58
|
+
'{requests: [{insertText: {location: {index: 1}, text: $text}}]}')
|
|
59
|
+
|
|
60
|
+
curl -s -X POST \
|
|
61
|
+
"https://docs.googleapis.com/v1/documents/${DOC_ID}:batchUpdate" \
|
|
62
|
+
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
63
|
+
-H "Content-Type: application/json" \
|
|
64
|
+
-d "$UPDATE_BODY" > /dev/null
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
echo "Created Google Doc: $TITLE"
|
|
68
|
+
echo "Doc ID: $DOC_ID"
|
|
69
|
+
echo "URL: https://docs.google.com/document/d/${DOC_ID}/edit"
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: google-drive
|
|
3
|
+
description: "Interact with Google Drive shared drives via service account. Supports list, upload, download, delete operations."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Google Drive
|
|
7
|
+
|
|
8
|
+
Interact with Google Drive shared drives using a service account.
|
|
9
|
+
|
|
10
|
+
## Environment Variables
|
|
11
|
+
|
|
12
|
+
- `GOOGLE_SERVICE_ACCOUNT_JSON` — Service account credentials JSON
|
|
13
|
+
- `GOOGLE_SHARED_DRIVE_ID` — Shared drive ID
|
|
14
|
+
|
|
15
|
+
## Commands
|
|
16
|
+
|
|
17
|
+
### List files in a folder
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
skills/google-drive/list.sh <folder_id>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Lists files and folders in the given folder. Use `$GOOGLE_SHARED_DRIVE_ID` for the root.
|
|
24
|
+
|
|
25
|
+
### Upload a file
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
skills/google-drive/upload.sh <local_file> <parent_folder_id> <filename>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Uploads a local file to the specified folder with the given name.
|
|
32
|
+
|
|
33
|
+
### Download a file
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
skills/google-drive/download.sh <file_id> <local_path>
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Downloads a file by ID to a local path.
|
|
40
|
+
|
|
41
|
+
### Delete a file
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
skills/google-drive/delete.sh <file_id>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Deletes a file by ID.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
FILE_ID="${1:?Usage: delete.sh <file_id>}"
|
|
5
|
+
|
|
6
|
+
if [ -z "${GOOGLE_SERVICE_ACCOUNT_JSON:-}" ]; then
|
|
7
|
+
echo "Error: GOOGLE_SERVICE_ACCOUNT_JSON is not set" >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
# Extract service account email and private key
|
|
12
|
+
SA_EMAIL=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.client_email')
|
|
13
|
+
SA_KEY=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.private_key')
|
|
14
|
+
TOKEN_URI=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.token_uri')
|
|
15
|
+
|
|
16
|
+
# Build JWT
|
|
17
|
+
NOW=$(date +%s)
|
|
18
|
+
EXP=$((NOW + 3600))
|
|
19
|
+
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
20
|
+
CLAIMS=$(printf '{"iss":"%s","scope":"https://www.googleapis.com/auth/drive","aud":"%s","iat":%d,"exp":%d}' \
|
|
21
|
+
"$SA_EMAIL" "$TOKEN_URI" "$NOW" "$EXP" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
22
|
+
SIGNING_INPUT="${HEADER}.${CLAIMS}"
|
|
23
|
+
SIGNATURE=$(printf '%s' "$SIGNING_INPUT" | openssl dgst -sha256 -sign <(echo "$SA_KEY") | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
24
|
+
JWT="${SIGNING_INPUT}.${SIGNATURE}"
|
|
25
|
+
|
|
26
|
+
# Exchange JWT for access token
|
|
27
|
+
ACCESS_TOKEN=$(curl -s -X POST "$TOKEN_URI" \
|
|
28
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
29
|
+
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${JWT}" | jq -r '.access_token')
|
|
30
|
+
|
|
31
|
+
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
|
|
32
|
+
echo "Error: Failed to obtain access token" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Delete file
|
|
37
|
+
HTTP_CODE=$(curl -s -w "%{http_code}" -o /dev/null \
|
|
38
|
+
-X DELETE \
|
|
39
|
+
"https://www.googleapis.com/drive/v3/files/${FILE_ID}?supportsAllDrives=true" \
|
|
40
|
+
-H "Authorization: Bearer ${ACCESS_TOKEN}")
|
|
41
|
+
|
|
42
|
+
if [ "$HTTP_CODE" -ne 204 ] && [ "$HTTP_CODE" -ne 200 ]; then
|
|
43
|
+
echo "Error: Delete failed with HTTP $HTTP_CODE" >&2
|
|
44
|
+
exit 1
|
|
45
|
+
fi
|
|
46
|
+
|
|
47
|
+
echo "Deleted file: $FILE_ID"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
FILE_ID="${1:?Usage: download.sh <file_id> <local_path>}"
|
|
5
|
+
LOCAL_PATH="${2:?Usage: download.sh <file_id> <local_path>}"
|
|
6
|
+
|
|
7
|
+
if [ -z "${GOOGLE_SERVICE_ACCOUNT_JSON:-}" ]; then
|
|
8
|
+
echo "Error: GOOGLE_SERVICE_ACCOUNT_JSON is not set" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Extract service account email and private key
|
|
13
|
+
SA_EMAIL=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.client_email')
|
|
14
|
+
SA_KEY=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.private_key')
|
|
15
|
+
TOKEN_URI=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.token_uri')
|
|
16
|
+
|
|
17
|
+
# Build JWT
|
|
18
|
+
NOW=$(date +%s)
|
|
19
|
+
EXP=$((NOW + 3600))
|
|
20
|
+
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
21
|
+
CLAIMS=$(printf '{"iss":"%s","scope":"https://www.googleapis.com/auth/drive","aud":"%s","iat":%d,"exp":%d}' \
|
|
22
|
+
"$SA_EMAIL" "$TOKEN_URI" "$NOW" "$EXP" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
23
|
+
SIGNING_INPUT="${HEADER}.${CLAIMS}"
|
|
24
|
+
SIGNATURE=$(printf '%s' "$SIGNING_INPUT" | openssl dgst -sha256 -sign <(echo "$SA_KEY") | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
25
|
+
JWT="${SIGNING_INPUT}.${SIGNATURE}"
|
|
26
|
+
|
|
27
|
+
# Exchange JWT for access token
|
|
28
|
+
ACCESS_TOKEN=$(curl -s -X POST "$TOKEN_URI" \
|
|
29
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
30
|
+
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${JWT}" | jq -r '.access_token')
|
|
31
|
+
|
|
32
|
+
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
|
|
33
|
+
echo "Error: Failed to obtain access token" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Download file
|
|
38
|
+
HTTP_CODE=$(curl -s -w "%{http_code}" -o "$LOCAL_PATH" \
|
|
39
|
+
"https://www.googleapis.com/drive/v3/files/${FILE_ID}?alt=media&supportsAllDrives=true&includeItemsFromAllDrives=true" \
|
|
40
|
+
-H "Authorization: Bearer ${ACCESS_TOKEN}")
|
|
41
|
+
|
|
42
|
+
if [ "$HTTP_CODE" -ne 200 ]; then
|
|
43
|
+
echo "Error: Download failed with HTTP $HTTP_CODE" >&2
|
|
44
|
+
cat "$LOCAL_PATH" >&2
|
|
45
|
+
rm -f "$LOCAL_PATH"
|
|
46
|
+
exit 1
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
FILE_SIZE=$(stat -c%s "$LOCAL_PATH" 2>/dev/null || stat -f%z "$LOCAL_PATH" 2>/dev/null)
|
|
50
|
+
echo "Downloaded to: $LOCAL_PATH ($FILE_SIZE bytes)"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
FOLDER_ID="${1:?Usage: list.sh <folder_id>}"
|
|
5
|
+
|
|
6
|
+
if [ -z "${GOOGLE_SERVICE_ACCOUNT_JSON:-}" ]; then
|
|
7
|
+
echo "Error: GOOGLE_SERVICE_ACCOUNT_JSON is not set" >&2
|
|
8
|
+
exit 1
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
# Extract service account email and private key
|
|
12
|
+
SA_EMAIL=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.client_email')
|
|
13
|
+
SA_KEY=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.private_key')
|
|
14
|
+
TOKEN_URI=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.token_uri')
|
|
15
|
+
|
|
16
|
+
# Build JWT
|
|
17
|
+
NOW=$(date +%s)
|
|
18
|
+
EXP=$((NOW + 3600))
|
|
19
|
+
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
20
|
+
CLAIMS=$(printf '{"iss":"%s","scope":"https://www.googleapis.com/auth/drive","aud":"%s","iat":%d,"exp":%d}' \
|
|
21
|
+
"$SA_EMAIL" "$TOKEN_URI" "$NOW" "$EXP" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
22
|
+
SIGNING_INPUT="${HEADER}.${CLAIMS}"
|
|
23
|
+
SIGNATURE=$(printf '%s' "$SIGNING_INPUT" | openssl dgst -sha256 -sign <(echo "$SA_KEY") | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
24
|
+
JWT="${SIGNING_INPUT}.${SIGNATURE}"
|
|
25
|
+
|
|
26
|
+
# Exchange JWT for access token
|
|
27
|
+
ACCESS_TOKEN=$(curl -s -X POST "$TOKEN_URI" \
|
|
28
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
29
|
+
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${JWT}" | jq -r '.access_token')
|
|
30
|
+
|
|
31
|
+
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
|
|
32
|
+
echo "Error: Failed to obtain access token" >&2
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# List files
|
|
37
|
+
RESPONSE=$(curl -s -X GET \
|
|
38
|
+
"https://www.googleapis.com/drive/v3/files?q='${FOLDER_ID}'+in+parents&supportsAllDrives=true&includeItemsFromAllDrives=true&corpora=allDrives&fields=files(id,name,mimeType,size,modifiedTime)" \
|
|
39
|
+
-H "Authorization: Bearer ${ACCESS_TOKEN}")
|
|
40
|
+
|
|
41
|
+
echo "$RESPONSE" | jq -r '.files[] | "\(.id)\t\(.name)\t\(.mimeType)\t\(.size // "N/A")\t\(.modifiedTime)"' 2>/dev/null || echo "$RESPONSE"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
LOCAL_FILE="${1:?Usage: upload.sh <local_file> <parent_folder_id> <filename>}"
|
|
5
|
+
PARENT_FOLDER_ID="${2:?Usage: upload.sh <local_file> <parent_folder_id> <filename>}"
|
|
6
|
+
FILENAME="${3:?Usage: upload.sh <local_file> <parent_folder_id> <filename>}"
|
|
7
|
+
|
|
8
|
+
if [ ! -f "$LOCAL_FILE" ]; then
|
|
9
|
+
echo "Error: File not found: $LOCAL_FILE" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
if [ -z "${GOOGLE_SERVICE_ACCOUNT_JSON:-}" ]; then
|
|
14
|
+
echo "Error: GOOGLE_SERVICE_ACCOUNT_JSON is not set" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
# Extract service account email and private key
|
|
19
|
+
SA_EMAIL=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.client_email')
|
|
20
|
+
SA_KEY=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.private_key')
|
|
21
|
+
TOKEN_URI=$(echo "$GOOGLE_SERVICE_ACCOUNT_JSON" | jq -r '.token_uri')
|
|
22
|
+
|
|
23
|
+
# Build JWT
|
|
24
|
+
NOW=$(date +%s)
|
|
25
|
+
EXP=$((NOW + 3600))
|
|
26
|
+
HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
27
|
+
CLAIMS=$(printf '{"iss":"%s","scope":"https://www.googleapis.com/auth/drive","aud":"%s","iat":%d,"exp":%d}' \
|
|
28
|
+
"$SA_EMAIL" "$TOKEN_URI" "$NOW" "$EXP" | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
29
|
+
SIGNING_INPUT="${HEADER}.${CLAIMS}"
|
|
30
|
+
SIGNATURE=$(printf '%s' "$SIGNING_INPUT" | openssl dgst -sha256 -sign <(echo "$SA_KEY") | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
|
|
31
|
+
JWT="${SIGNING_INPUT}.${SIGNATURE}"
|
|
32
|
+
|
|
33
|
+
# Exchange JWT for access token
|
|
34
|
+
ACCESS_TOKEN=$(curl -s -X POST "$TOKEN_URI" \
|
|
35
|
+
-H "Content-Type: application/x-www-form-urlencoded" \
|
|
36
|
+
-d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${JWT}" | jq -r '.access_token')
|
|
37
|
+
|
|
38
|
+
if [ -z "$ACCESS_TOKEN" ] || [ "$ACCESS_TOKEN" = "null" ]; then
|
|
39
|
+
echo "Error: Failed to obtain access token" >&2
|
|
40
|
+
exit 1
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# Detect MIME type from extension
|
|
44
|
+
EXT="${LOCAL_FILE##*.}"
|
|
45
|
+
case "${EXT,,}" in
|
|
46
|
+
jpg|jpeg) MIME_TYPE="image/jpeg" ;;
|
|
47
|
+
png) MIME_TYPE="image/png" ;;
|
|
48
|
+
gif) MIME_TYPE="image/gif" ;;
|
|
49
|
+
webp) MIME_TYPE="image/webp" ;;
|
|
50
|
+
mp4) MIME_TYPE="video/mp4" ;;
|
|
51
|
+
webm) MIME_TYPE="video/webm" ;;
|
|
52
|
+
pdf) MIME_TYPE="application/pdf" ;;
|
|
53
|
+
txt) MIME_TYPE="text/plain" ;;
|
|
54
|
+
json) MIME_TYPE="application/json" ;;
|
|
55
|
+
csv) MIME_TYPE="text/csv" ;;
|
|
56
|
+
*) MIME_TYPE="application/octet-stream" ;;
|
|
57
|
+
esac
|
|
58
|
+
|
|
59
|
+
# Upload file using multipart upload
|
|
60
|
+
METADATA=$(printf '{"name":"%s","parents":["%s"]}' "$FILENAME" "$PARENT_FOLDER_ID")
|
|
61
|
+
|
|
62
|
+
RESPONSE=$(curl -s -X POST \
|
|
63
|
+
"https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&supportsAllDrives=true&includeItemsFromAllDrives=true" \
|
|
64
|
+
-H "Authorization: Bearer ${ACCESS_TOKEN}" \
|
|
65
|
+
-F "metadata=${METADATA};type=application/json" \
|
|
66
|
+
-F "file=@${LOCAL_FILE};type=${MIME_TYPE}")
|
|
67
|
+
|
|
68
|
+
FILE_ID=$(echo "$RESPONSE" | jq -r '.id')
|
|
69
|
+
if [ -z "$FILE_ID" ] || [ "$FILE_ID" = "null" ]; then
|
|
70
|
+
echo "Error: Upload failed" >&2
|
|
71
|
+
echo "$RESPONSE" >&2
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
echo "Uploaded: $FILENAME"
|
|
76
|
+
echo "File ID: $FILE_ID"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: kie-ai
|
|
3
|
+
description: "Generate images and videos using kie.ai API. Use for AI image and video generation tasks."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# KIE AI
|
|
7
|
+
|
|
8
|
+
Generate images and videos using the kie.ai API.
|
|
9
|
+
|
|
10
|
+
## Environment Variables
|
|
11
|
+
|
|
12
|
+
- `KIE_AI_API_KEY` — API key for kie.ai
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
### Generate an image
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
skills/kie-ai/generate-image.sh <prompt> [aspect_ratio] [resolution] [output_format]
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- `prompt` (required) — Text prompt for image generation
|
|
23
|
+
- `aspect_ratio` (optional, default: `auto`) — One of: `auto, 1:1, 1:4, 16:9, 1:8, 21:9, 2:3, 3:2, 3:4, 4:1, 4:3, 4:5, 5:4, 8:1, 9:16`
|
|
24
|
+
- `resolution` (optional, default: `1K`) — One of: `1K, 2K, 4K`
|
|
25
|
+
- `output_format` (optional, default: `jpg`) — One of: `jpg, png`
|
|
26
|
+
|
|
27
|
+
Downloads the generated image to a local file and prints the path.
|
|
28
|
+
|
|
29
|
+
### Generate a video
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
skills/kie-ai/generate-video.sh <prompt> [aspect_ratio]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
- `prompt` (required) — Text prompt for video generation
|
|
36
|
+
- `aspect_ratio` (optional, default: `16:9`) — One of: `16:9, 9:16, Auto`
|
|
37
|
+
|
|
38
|
+
Downloads the generated video to a local file and prints the path.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PROMPT="${1:?Usage: generate-image.sh <prompt> [aspect_ratio] [resolution] [output_format]}"
|
|
5
|
+
ASPECT_RATIO="${2:-auto}"
|
|
6
|
+
RESOLUTION="${3:-1K}"
|
|
7
|
+
OUTPUT_FORMAT="${4:-jpg}"
|
|
8
|
+
|
|
9
|
+
if [ -z "${KIE_AI_API_KEY:-}" ]; then
|
|
10
|
+
echo "Error: KIE_AI_API_KEY is not set" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Create task
|
|
15
|
+
BODY=$(jq -n \
|
|
16
|
+
--arg prompt "$PROMPT" \
|
|
17
|
+
--arg ar "$ASPECT_RATIO" \
|
|
18
|
+
--arg res "$RESOLUTION" \
|
|
19
|
+
--arg fmt "$OUTPUT_FORMAT" \
|
|
20
|
+
'{
|
|
21
|
+
model: "nano-banana-2",
|
|
22
|
+
input: {
|
|
23
|
+
prompt: $prompt,
|
|
24
|
+
aspect_ratio: $ar,
|
|
25
|
+
resolution: $res,
|
|
26
|
+
output_format: $fmt
|
|
27
|
+
}
|
|
28
|
+
}')
|
|
29
|
+
|
|
30
|
+
echo "Creating image generation task..." >&2
|
|
31
|
+
CREATE_RESPONSE=$(curl -s -X POST \
|
|
32
|
+
"https://api.kie.ai/api/v1/jobs/createTask" \
|
|
33
|
+
-H "Authorization: Bearer ${KIE_AI_API_KEY}" \
|
|
34
|
+
-H "Content-Type: application/json" \
|
|
35
|
+
-d "$BODY")
|
|
36
|
+
|
|
37
|
+
TASK_ID=$(echo "$CREATE_RESPONSE" | jq -r '.data.taskId // .taskId // empty')
|
|
38
|
+
if [ -z "$TASK_ID" ]; then
|
|
39
|
+
echo "Error: Failed to create task" >&2
|
|
40
|
+
echo "$CREATE_RESPONSE" >&2
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
echo "Task ID: $TASK_ID — polling for completion..." >&2
|
|
45
|
+
|
|
46
|
+
# Poll for completion
|
|
47
|
+
while true; do
|
|
48
|
+
sleep 15
|
|
49
|
+
STATUS_RESPONSE=$(curl -s -X GET \
|
|
50
|
+
"https://api.kie.ai/api/v1/jobs/recordInfo?taskId=${TASK_ID}" \
|
|
51
|
+
-H "Authorization: Bearer ${KIE_AI_API_KEY}" \
|
|
52
|
+
-H "Content-Type: application/json")
|
|
53
|
+
|
|
54
|
+
STATE=$(echo "$STATUS_RESPONSE" | jq -r '.data.state // empty')
|
|
55
|
+
echo "State: $STATE" >&2
|
|
56
|
+
|
|
57
|
+
if [ "$STATE" = "success" ]; then
|
|
58
|
+
# .data.resultJson is a JSON string — parse it, then get .resultUrls[0]
|
|
59
|
+
IMAGE_URL=$(echo "$STATUS_RESPONSE" | jq -r '.data.resultJson' | jq -r '.resultUrls[0]')
|
|
60
|
+
if [ -z "$IMAGE_URL" ] || [ "$IMAGE_URL" = "null" ]; then
|
|
61
|
+
echo "Error: Could not extract image URL" >&2
|
|
62
|
+
echo "$STATUS_RESPONSE" >&2
|
|
63
|
+
exit 1
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# Download
|
|
67
|
+
OUTPUT_FILE="/tmp/kie-image-${TASK_ID}.${OUTPUT_FORMAT}"
|
|
68
|
+
curl -s -o "$OUTPUT_FILE" "$IMAGE_URL"
|
|
69
|
+
FILE_SIZE=$(stat -c%s "$OUTPUT_FILE" 2>/dev/null || stat -f%z "$OUTPUT_FILE" 2>/dev/null)
|
|
70
|
+
echo "Image downloaded: $OUTPUT_FILE ($FILE_SIZE bytes)"
|
|
71
|
+
exit 0
|
|
72
|
+
elif [ "$STATE" = "failed" ] || [ "$STATE" = "error" ]; then
|
|
73
|
+
echo "Error: Image generation failed" >&2
|
|
74
|
+
echo "$STATUS_RESPONSE" >&2
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
done
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PROMPT="${1:?Usage: generate-video.sh <prompt> [aspect_ratio]}"
|
|
5
|
+
ASPECT_RATIO="${2:-16:9}"
|
|
6
|
+
|
|
7
|
+
if [ -z "${KIE_AI_API_KEY:-}" ]; then
|
|
8
|
+
echo "Error: KIE_AI_API_KEY is not set" >&2
|
|
9
|
+
exit 1
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
# Create task
|
|
13
|
+
BODY=$(jq -n \
|
|
14
|
+
--arg prompt "$PROMPT" \
|
|
15
|
+
--arg ar "$ASPECT_RATIO" \
|
|
16
|
+
'{
|
|
17
|
+
prompt: $prompt,
|
|
18
|
+
model: "veo3_fast",
|
|
19
|
+
generationType: "TEXT_2_VIDEO",
|
|
20
|
+
aspect_ratio: $ar
|
|
21
|
+
}')
|
|
22
|
+
|
|
23
|
+
echo "Creating video generation task..." >&2
|
|
24
|
+
CREATE_RESPONSE=$(curl -s -X POST \
|
|
25
|
+
"https://api.kie.ai/api/v1/veo/generate" \
|
|
26
|
+
-H "Authorization: Bearer ${KIE_AI_API_KEY}" \
|
|
27
|
+
-H "Content-Type: application/json" \
|
|
28
|
+
-d "$BODY")
|
|
29
|
+
|
|
30
|
+
TASK_ID=$(echo "$CREATE_RESPONSE" | jq -r '.data.taskId // .taskId // empty')
|
|
31
|
+
if [ -z "$TASK_ID" ]; then
|
|
32
|
+
echo "Error: Failed to create task" >&2
|
|
33
|
+
echo "$CREATE_RESPONSE" >&2
|
|
34
|
+
exit 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
echo "Task ID: $TASK_ID — polling for completion..." >&2
|
|
38
|
+
|
|
39
|
+
# Poll for completion
|
|
40
|
+
while true; do
|
|
41
|
+
sleep 15
|
|
42
|
+
STATUS_RESPONSE=$(curl -s -X GET \
|
|
43
|
+
"https://api.kie.ai/api/v1/veo/record-info?taskId=${TASK_ID}" \
|
|
44
|
+
-H "Authorization: Bearer ${KIE_AI_API_KEY}" \
|
|
45
|
+
-H "Content-Type: application/json")
|
|
46
|
+
|
|
47
|
+
SUCCESS_FLAG=$(echo "$STATUS_RESPONSE" | jq -r '.data.successFlag // empty')
|
|
48
|
+
echo "Status check... successFlag=$SUCCESS_FLAG" >&2
|
|
49
|
+
|
|
50
|
+
if [ "$SUCCESS_FLAG" = "1" ]; then
|
|
51
|
+
VIDEO_URL=$(echo "$STATUS_RESPONSE" | jq -r '.data.response.resultUrls[0]')
|
|
52
|
+
if [ -z "$VIDEO_URL" ] || [ "$VIDEO_URL" = "null" ]; then
|
|
53
|
+
echo "Error: Could not extract video URL" >&2
|
|
54
|
+
echo "$STATUS_RESPONSE" >&2
|
|
55
|
+
exit 1
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
# Download
|
|
59
|
+
OUTPUT_FILE="/tmp/kie-video-${TASK_ID}.mp4"
|
|
60
|
+
curl -s -o "$OUTPUT_FILE" "$VIDEO_URL"
|
|
61
|
+
FILE_SIZE=$(stat -c%s "$OUTPUT_FILE" 2>/dev/null || stat -f%z "$OUTPUT_FILE" 2>/dev/null)
|
|
62
|
+
echo "Video downloaded: $OUTPUT_FILE ($FILE_SIZE bytes)"
|
|
63
|
+
exit 0
|
|
64
|
+
elif [ "$SUCCESS_FLAG" = "-1" ]; then
|
|
65
|
+
echo "Error: Video generation failed" >&2
|
|
66
|
+
echo "$STATUS_RESPONSE" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
done
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: youtube-transcript
|
|
3
|
+
description: Fetch transcripts from YouTube videos for summarization and analysis.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# YouTube Transcript
|
|
7
|
+
|
|
8
|
+
Fetch transcripts from YouTube videos.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
cd skills/youtube-transcript
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
skills/youtube-transcript/transcript.js <video-id-or-url>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Accepts video ID or full URL:
|
|
24
|
+
- `EBw7gsDPAYQ`
|
|
25
|
+
- `https://www.youtube.com/watch?v=EBw7gsDPAYQ`
|
|
26
|
+
- `https://youtu.be/EBw7gsDPAYQ`
|
|
27
|
+
|
|
28
|
+
## Output
|
|
29
|
+
|
|
30
|
+
Timestamped transcript entries:
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
[0:00] All right. So, I got this UniFi Theta
|
|
34
|
+
[0:15] I took the camera out, painted it
|
|
35
|
+
[1:23] And here's the final result
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Notes
|
|
39
|
+
|
|
40
|
+
- Requires the video to have captions/transcripts available
|
|
41
|
+
- Works with auto-generated and manual transcripts
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "youtube-transcript-skill",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "youtube-transcript-skill",
|
|
9
|
+
"version": "1.0.0",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"youtube-transcript-plus": "^1.2.0"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"node_modules/youtube-transcript-plus": {
|
|
15
|
+
"version": "1.2.0",
|
|
16
|
+
"resolved": "https://registry.npmjs.org/youtube-transcript-plus/-/youtube-transcript-plus-1.2.0.tgz",
|
|
17
|
+
"integrity": "sha512-SRjVft8V+vUulMKgakgfzC+pnFLSy4tolX7xGnSvp9juUNocikMFmUx5GlhzLDILzxYrijcYtmNqz0qyklnPmA==",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { YoutubeTranscript } from 'youtube-transcript-plus';
|
|
4
|
+
|
|
5
|
+
function extractVideoId(input) {
|
|
6
|
+
if (!input) return null;
|
|
7
|
+
|
|
8
|
+
// Already a plain video ID (11 characters, alphanumeric + dash/underscore)
|
|
9
|
+
if (/^[A-Za-z0-9_-]{11}$/.test(input)) {
|
|
10
|
+
return input;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const url = new URL(input);
|
|
15
|
+
|
|
16
|
+
// youtu.be/VIDEO_ID
|
|
17
|
+
if (url.hostname === 'youtu.be') {
|
|
18
|
+
return url.pathname.slice(1);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// youtube.com/watch?v=VIDEO_ID
|
|
22
|
+
if (url.hostname === 'www.youtube.com' || url.hostname === 'youtube.com') {
|
|
23
|
+
return url.searchParams.get('v');
|
|
24
|
+
}
|
|
25
|
+
} catch {
|
|
26
|
+
// Not a URL — treat as video ID anyway
|
|
27
|
+
return input;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function decodeHtmlEntities(str) {
|
|
34
|
+
return str
|
|
35
|
+
.replace(/'/g, "'")
|
|
36
|
+
.replace(/&/g, '&')
|
|
37
|
+
.replace(/</g, '<')
|
|
38
|
+
.replace(/>/g, '>')
|
|
39
|
+
.replace(/"/g, '"')
|
|
40
|
+
.replace(/&#(\d+);/g, (_, num) => String.fromCharCode(Number(num)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function formatTime(seconds) {
|
|
44
|
+
const totalSeconds = Math.floor(seconds);
|
|
45
|
+
const mins = Math.floor(totalSeconds / 60);
|
|
46
|
+
const secs = totalSeconds % 60;
|
|
47
|
+
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function main() {
|
|
51
|
+
const input = process.argv[2];
|
|
52
|
+
|
|
53
|
+
if (!input) {
|
|
54
|
+
console.error('Usage: transcript.js <video-id-or-url>');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const videoId = extractVideoId(input);
|
|
59
|
+
|
|
60
|
+
if (!videoId) {
|
|
61
|
+
console.error(`Error: Could not extract video ID from "${input}"`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const transcript = await YoutubeTranscript.fetchTranscript(videoId);
|
|
67
|
+
|
|
68
|
+
if (!transcript || transcript.length === 0) {
|
|
69
|
+
console.error('No transcript available for this video.');
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const entry of transcript) {
|
|
74
|
+
const time = formatTime(entry.offset);
|
|
75
|
+
const text = decodeHtmlEntities(entry.text.replace(/\n/g, ' ')).trim();
|
|
76
|
+
console.log(`[${time}] ${text}`);
|
|
77
|
+
}
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(`Error fetching transcript: ${err.message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
main();
|