Compare commits

...

14 Commits

Author SHA1 Message Date
Philipinho bb83d12c8b AI module - init 2025-08-15 23:18:51 -07:00
Philipinho 0f29eb8842 WIP 2025-08-14 23:13:23 -07:00
Philipinho 08135a2fba sync 2025-08-12 11:09:26 -07:00
Philipinho d92a94244f sync 2025-08-12 10:21:17 -07:00
Philipinho 5012a68d85 sync 2025-08-06 10:19:35 -07:00
Philip Okugbe 5a3377790e feat: debug mode env variable (#1450) 2025-08-06 18:16:30 +01:00
Philip Okugbe 3b85f4b616 fix: enforce C collation for page position ordering to ensure consistent behavior in Postgres 17+ (#1446)
- Add explicit C collation to position ordering queries to fix incorrect page placement in PostgreSQL 17+
- Ensures consistent ASCII-based ordering regardless of database locale settings
- Fixes issue where new pages were incorrectly placed at random positions instead of bottom
2025-08-04 09:49:29 +01:00
Philipinho cb2a0398c7 fix: invalidate trashed page from tree state 2025-08-04 00:42:13 -07:00
Philip Okugbe 95b7be61df fix: hide trash from can view permission (#1445) 2025-08-04 08:35:28 +01:00
Philip Okugbe b0c557272d fix nested taskList in markdown export (#1443) 2025-08-04 08:01:18 +01:00
Philip Okugbe dddfd48934 feat: add attachments support for single page exports (#1440)
* feat: add attachments support for single page exports
- Add includeAttachments option to page export modal and API
- Fix internal page url in single page exports in cloud

* remove redundant line

* preserve export state
2025-08-04 08:01:11 +01:00
Philipinho aa6eec754e fix: exclude trashed pages from position generation 2025-08-04 00:00:06 -07:00
Philip Okugbe 97a7701f5d fix local storage copy function (#1442) 2025-08-04 03:20:18 +01:00
Philipinho b97eb85d05 sync 2025-08-03 03:59:08 -07:00
26 changed files with 393 additions and 164 deletions
+3
View File
@@ -44,3 +44,6 @@ POSTMARK_TOKEN=
DRAWIO_URL= DRAWIO_URL=
DISABLE_TELEMETRY=false DISABLE_TELEMETRY=false
# Enable debug logging in production (default: false)
DEBUG_MODE=false
+2 -5
View File
@@ -34,7 +34,7 @@ import { useTrackOrigin } from "@/hooks/use-track-origin";
import SpacesPage from "@/pages/spaces/spaces.tsx"; import SpacesPage from "@/pages/spaces/spaces.tsx";
import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page"; import { MfaChallengePage } from "@/ee/mfa/pages/mfa-challenge-page";
import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page"; import { MfaSetupRequiredPage } from "@/ee/mfa/pages/mfa-setup-required-page";
import SpaceTrash from "@/pages/space/trash.tsx"; import SpaceTrash from "@/pages/space/space-trash.tsx";
export default function App() { export default function App() {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -50,10 +50,7 @@ export default function App() {
<Route path={"/forgot-password"} element={<ForgotPassword />} /> <Route path={"/forgot-password"} element={<ForgotPassword />} />
<Route path={"/password-reset"} element={<PasswordReset />} /> <Route path={"/password-reset"} element={<PasswordReset />} />
<Route path={"/login/mfa"} element={<MfaChallengePage />} /> <Route path={"/login/mfa"} element={<MfaChallengePage />} />
<Route <Route path={"/login/mfa/setup"} element={<MfaSetupRequiredPage />} />
path={"/login/mfa/setup"}
element={<MfaSetupRequiredPage />}
/>
{!isCloud() && ( {!isCloud() && (
<Route path={"/setup/register"} element={<SetupWorkspace />} /> <Route path={"/setup/register"} element={<SetupWorkspace />} />
@@ -29,19 +29,22 @@ export default function ExportModal({
}: ExportModalProps) { }: ExportModalProps) {
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown); const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
const [includeChildren, setIncludeChildren] = useState<boolean>(false); const [includeChildren, setIncludeChildren] = useState<boolean>(false);
const [includeAttachments, setIncludeAttachments] = useState<boolean>(true); const [includeAttachments, setIncludeAttachments] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
const handleExport = async () => { const handleExport = async () => {
try { try {
if (type === "page") { if (type === "page") {
await exportPage({ pageId: id, format, includeChildren }); await exportPage({
pageId: id,
format,
includeChildren,
includeAttachments,
});
} }
if (type === "space") { if (type === "space") {
await exportSpace({ spaceId: id, format, includeAttachments }); await exportSpace({ spaceId: id, format, includeAttachments });
} }
setIncludeChildren(false);
setIncludeAttachments(true);
onClose(); onClose();
} catch (err) { } catch (err) {
notifications.show({ notifications.show({
@@ -96,6 +99,18 @@ export default function ExportModal({
checked={includeChildren} checked={includeChildren}
/> />
</Group> </Group>
<Group justify="space-between" wrap="nowrap" mt="md">
<div>
<Text size="md">{t("Include attachments")}</Text>
</div>
<Switch
onChange={(event) =>
setIncludeAttachments(event.currentTarget.checked)
}
checked={includeAttachments}
/>
</Group>
</> </>
)} )}
+61
View File
@@ -0,0 +1,61 @@
import { useState, useCallback, useRef } from "react";
import { useAiGenerateStreamMutation } from "@/ee/ai/queries/ai-query.ts";
import { AiGenerateDto } from "@/ee/ai/types/ai.types.ts";
export function useAiStream() {
const [content, setContent] = useState("");
const [isStreaming, setIsStreaming] = useState(false);
const abortControllerRef = useRef<AbortController | null>(null);
const mutation = useAiGenerateStreamMutation();
const startStream = useCallback(
async (data: AiGenerateDto) => {
setContent("");
setIsStreaming(true);
try {
const controller = await mutation.mutateAsync({
...data,
onChunk: (chunk) => {
setContent((prev) => prev + chunk.content);
},
onError: (error) => {
console.error("AI stream error:", error);
setIsStreaming(false);
},
onComplete: () => {
setIsStreaming(false);
},
});
abortControllerRef.current = controller;
} catch (error) {
console.error("Failed to start stream:", error);
setIsStreaming(false);
}
},
[mutation]
);
const stopStream = useCallback(() => {
if (abortControllerRef.current) {
abortControllerRef.current.abort();
abortControllerRef.current = null;
setIsStreaming(false);
}
}, []);
const resetContent = useCallback(() => {
setContent("");
}, []);
return {
content,
isStreaming,
startStream,
stopStream,
resetContent,
isLoading: mutation.isPending,
error: mutation.error,
};
}
+45
View File
@@ -0,0 +1,45 @@
import {
useMutation,
UseMutationResult,
useQuery,
UseQueryResult,
} from "@tanstack/react-query";
import {
generateAiContent,
generateAiContentStream,
getAiConfig,
} from "@/ee/ai/services/ai-service.ts";
import {
AiConfigResponse,
AiContentResponse,
AiGenerateDto,
AiStreamChunk,
AiStreamError,
} from "@/ee/ai/types/ai.types.ts";
export function useAiGenerateMutation(): UseMutationResult<
AiContentResponse,
Error,
AiGenerateDto
> {
return useMutation({
mutationFn: (data: AiGenerateDto) => generateAiContent(data),
});
}
interface StreamCallbacks {
onChunk: (chunk: AiStreamChunk) => void;
onError?: (error: AiStreamError) => void;
onComplete?: () => void;
}
export function useAiGenerateStreamMutation(): UseMutationResult<
AbortController,
Error,
AiGenerateDto & StreamCallbacks
> {
return useMutation({
mutationFn: ({ onChunk, onError, onComplete, ...data }) =>
generateAiContentStream(data, onChunk, onError, onComplete),
});
}
@@ -0,0 +1,89 @@
import api from "@/lib/api-client.ts";
import {
AiGenerateDto,
AiContentResponse,
AiStreamChunk,
AiStreamError,
} from "@/ee/ai/types/ai.types.ts";
export async function generateAiContent(
data: AiGenerateDto,
): Promise<AiContentResponse> {
const req = await api.post<AiContentResponse>("/ai/generate", data);
return req.data;
}
export async function generateAiContentStream(
data: AiGenerateDto,
onChunk: (chunk: AiStreamChunk) => void,
onError?: (error: AiStreamError) => void,
onComplete?: () => void,
): Promise<AbortController> {
const abortController = new AbortController();
try {
const response = await fetch("/api/ai/generate/stream", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
signal: abortController.signal,
credentials: "include", // This ensures cookies are sent, matching axios withCredentials
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body?.getReader();
const decoder = new TextDecoder();
if (!reader) {
throw new Error("Response body is not readable");
}
const processStream = async () => {
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
const lines = chunk.split("\n");
for (const line of lines) {
if (line.startsWith("data: ")) {
const data = line.slice(6);
if (data === "[DONE]") {
onComplete?.();
return;
}
try {
const parsed = JSON.parse(data);
if (parsed.error) {
onError?.(parsed);
} else {
onChunk(parsed);
}
} catch (e) {
// Ignore parse errors for incomplete chunks
}
}
}
}
} catch (error) {
if (error.name !== "AbortError") {
onError?.({ error: error.message });
}
} finally {
reader.releaseLock();
}
};
processStream();
} catch (error) {
onError?.({ error: error.message });
}
return abortController;
}
+40
View File
@@ -0,0 +1,40 @@
export enum AiAction {
IMPROVE_WRITING = "improve_writing",
FIX_SPELLING_GRAMMAR = "fix_spelling_grammar",
MAKE_SHORTER = "make_shorter",
MAKE_LONGER = "make_longer",
SIMPLIFY = "simplify",
CHANGE_TONE = "change_tone",
SUMMARIZE = "summarize",
CONTINUE_WRITING = "continue_writing",
TRANSLATE = "translate",
CUSTOM = "custom",
}
export interface AiGenerateDto {
action?: AiAction;
content: string;
prompt?: string;
}
export interface AiContentResponse {
content: string;
usage?: {
promptTokens: number;
completionTokens: number;
totalTokens: number;
};
}
export interface AiConfigResponse {
configured: boolean;
availableActions: AiAction[];
}
export interface AiStreamChunk {
content: string;
}
export interface AiStreamError {
error: string;
}
@@ -1,105 +0,0 @@
import { Modal, Button, Group, Text, Select, Switch } from "@mantine/core";
import { exportPage } from "@/features/page/services/page-service.ts";
import { useState } from "react";
import * as React from "react";
import { ExportFormat } from "@/features/page/types/page.types.ts";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
interface PageExportModalProps {
pageId: string;
open: boolean;
onClose: () => void;
}
export default function PageExportModal({
pageId,
open,
onClose,
}: PageExportModalProps) {
const { t } = useTranslation();
const [format, setFormat] = useState<ExportFormat>(ExportFormat.Markdown);
const handleExport = async () => {
try {
await exportPage({ pageId: pageId, format });
onClose();
} catch (err) {
notifications.show({
message: t("Export failed:") + err.response?.data.message,
color: "red",
});
console.error("export error", err);
}
};
const handleChange = (format: ExportFormat) => {
setFormat(format);
};
return (
<Modal.Root
opened={open}
onClose={onClose}
size={500}
padding="xl"
yOffset="10vh"
xOffset={0}
mah={400}
>
<Modal.Overlay />
<Modal.Content style={{ overflow: "hidden" }}>
<Modal.Header py={0}>
<Modal.Title fw={500}>{t("Export page")}</Modal.Title>
<Modal.CloseButton />
</Modal.Header>
<Modal.Body>
<Group justify="space-between" wrap="nowrap">
<div>
<Text size="md">{t("Format")}</Text>
</div>
<ExportFormatSelection format={format} onChange={handleChange} />
</Group>
<Group justify="space-between" wrap="nowrap" pt="md">
<div>
<Text size="md">{t("Include subpages")}</Text>
</div>
<Switch defaultChecked />
</Group>
<Group justify="center" mt="md">
<Button onClick={onClose} variant="default">
{t("Cancel")}
</Button>
<Button onClick={handleExport}>{t("Export")}</Button>
</Group>
</Modal.Body>
</Modal.Content>
</Modal.Root>
);
}
interface ExportFormatSelection {
format: ExportFormat;
onChange: (value: string) => void;
}
function ExportFormatSelection({ format, onChange }: ExportFormatSelection) {
const { t } = useTranslation();
return (
<Select
data={[
{ value: "markdown", label: "Markdown" },
{ value: "html", label: "HTML" },
]}
defaultValue={format}
onChange={onChange}
styles={{ wrapper: { maxWidth: 120 } }}
comboboxProps={{ width: "120" }}
allowDeselect={false}
withCheckIcon={false}
aria-label={t("Select export format")}
/>
);
}
@@ -126,8 +126,9 @@ export function useUpdatePageMutation() {
export function useRemovePageMutation() { export function useRemovePageMutation() {
return useMutation({ return useMutation({
mutationFn: (pageId: string) => deletePage(pageId, false), mutationFn: (pageId: string) => deletePage(pageId, false),
onSuccess: () => { onSuccess: (_, pageId) => {
notifications.show({ message: "Page moved to trash" }); notifications.show({ message: "Page moved to trash" });
invalidateOnDeletePage(pageId);
queryClient.invalidateQueries({ queryClient.invalidateQueries({
predicate: (item) => predicate: (item) =>
["trash-list"].includes(item.queryKey[0] as string), ["trash-list"].includes(item.queryKey[0] as string),
@@ -32,7 +32,7 @@ import { UserInfo } from "@/components/common/user-info.tsx";
import Paginate from "@/components/common/paginate.tsx"; import Paginate from "@/components/common/paginate.tsx";
import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search"; import { usePaginateAndSearch } from "@/hooks/use-paginate-and-search";
export default function SpaceTrash() { export default function Trash() {
const { t } = useTranslation(); const { t } = useTranslation();
const { spaceSlug } = useParams(); const { spaceSlug } = useParams();
const { page, setPage } = usePaginateAndSearch(); const { page, setPage } = usePaginateAndSearch();
@@ -79,6 +79,7 @@ export interface IExportPageParams {
pageId: string; pageId: string;
format: ExportFormat; format: ExportFormat;
includeChildren?: boolean; includeChildren?: boolean;
includeAttachments?: boolean;
} }
export enum ExportFormat { export enum ExportFormat {
@@ -0,0 +1,27 @@
import Trash from "@/features/page/trash/components/trash.tsx";
import { useParams } from "react-router-dom";
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
import React from "react";
import {
SpaceCaslAction,
SpaceCaslSubject,
} from "@/features/space/permissions/permissions.type.ts";
export default function SpaceTrash() {
const { spaceSlug } = useParams();
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
const spaceRules = space?.membership?.permissions;
const spaceAbility = useSpaceAbility(spaceRules);
if (!space) {
return <></>;
}
if (spaceAbility.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
return <></>;
}
return <Trash />;
}
+1
View File
@@ -70,6 +70,7 @@
"nanoid": "3.3.11", "nanoid": "3.3.11",
"nestjs-kysely": "^1.2.0", "nestjs-kysely": "^1.2.0",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.3",
"openai": "^5.12.2",
"openid-client": "^5.7.1", "openid-client": "^5.7.1",
"otpauth": "^9.4.0", "otpauth": "^9.4.0",
"p-limit": "^6.2.0", "p-limit": "^6.2.0",
@@ -12,10 +12,14 @@ export class InternalLogFilter extends ConsoleLogger {
constructor() { constructor() {
super(); super();
this.allowedLogLevels = const isProduction = process.env.NODE_ENV === 'production';
process.env.NODE_ENV === 'production' const isDebugMode = process.env.DEBUG_MODE === 'true';
? ['log', 'error', 'fatal']
: ['log', 'debug', 'verbose', 'warn', 'error', 'fatal']; if (isProduction && !isDebugMode) {
this.allowedLogLevels = ['log', 'error', 'fatal'];
} else {
this.allowedLogLevels = ['log', 'debug', 'verbose', 'warn', 'error', 'fatal'];
}
} }
private isLogLevelAllowed(level: string): boolean { private isLogLevelAllowed(level: string): boolean {
@@ -1,7 +1,7 @@
import { IsOptional, IsString } from 'class-validator'; import { IsNotEmpty, IsString } from 'class-validator';
export class DeletedPageDto { export class DeletedPageDto {
@IsOptional() @IsNotEmpty()
@IsString() @IsString()
spaceId: string; spaceId: string;
} }
+1 -1
View File
@@ -194,7 +194,7 @@ export class PageController {
deletedPageDto.spaceId, deletedPageDto.spaceId,
); );
if (ability.cannot(SpaceCaslAction.Read, SpaceCaslSubject.Page)) { if (ability.cannot(SpaceCaslAction.Manage, SpaceCaslSubject.Page)) {
throw new ForbiddenException(); throw new ForbiddenException();
} }
@@ -109,7 +109,8 @@ export class PageService {
.selectFrom('pages') .selectFrom('pages')
.select(['position']) .select(['position'])
.where('spaceId', '=', spaceId) .where('spaceId', '=', spaceId)
.orderBy('position', 'desc') .where('deletedAt', 'is', null)
.orderBy('position', (ob) => ob.collate('C').desc())
.limit(1); .limit(1);
if (parentPageId) { if (parentPageId) {
@@ -190,7 +191,7 @@ export class PageService {
'deletedAt', 'deletedAt',
]) ])
.select((eb) => this.pageRepo.withHasChildren(eb)) .select((eb) => this.pageRepo.withHasChildren(eb))
.orderBy('position', 'asc') .orderBy('position', (ob) => ob.collate('C').asc())
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.where('spaceId', '=', spaceId); .where('spaceId', '=', spaceId);
@@ -213,4 +213,16 @@ export class EnvironmentService {
getPostHogKey(): string { getPostHogKey(): string {
return this.configService.get<string>('POSTHOG_KEY'); return this.configService.get<string>('POSTHOG_KEY');
} }
getOpenAiApiKey(): string {
return this.configService.get<string>('OPENAI_API_KEY');
}
getOpenAiApiUrl(): string {
return this.configService.get<string>('OPENAI_API_URL');
}
getOpenAiModel(): string {
return this.configService.get<string>('OPENAI_MODEL');
}
} }
@@ -23,6 +23,10 @@ export class ExportPageDto {
@IsOptional() @IsOptional()
@IsBoolean() @IsBoolean()
includeChildren?: boolean; includeChildren?: boolean;
@IsOptional()
@IsBoolean()
includeAttachments?: boolean;
} }
export class ExportSpaceDto { export class ExportSpaceDto {
@@ -55,40 +55,22 @@ export class ExportController {
throw new ForbiddenException(); throw new ForbiddenException();
} }
const fileExt = getExportExtension(dto.format); const zipFileBuffer = await this.exportService.exportPages(
const fileName = sanitize(page.title || 'untitled') + fileExt; dto.pageId,
if (dto.includeChildren) {
const zipFileBuffer = await this.exportService.exportPageWithChildren(
dto.pageId,
dto.format,
);
const newName = path.parse(fileName).name + '.zip';
res.headers({
'Content-Type': 'application/zip',
'Content-Disposition':
'attachment; filename="' + encodeURIComponent(newName) + '"',
});
res.send(zipFileBuffer);
return;
}
const rawContent = await this.exportService.exportPage(
dto.format, dto.format,
page, dto.includeAttachments,
true, dto.includeChildren,
); );
const fileName = sanitize(page.title || 'untitled') + '.zip';
res.headers({ res.headers({
'Content-Type': getMimeType(fileExt), 'Content-Type': 'application/zip',
'Content-Disposition': 'Content-Disposition':
'attachment; filename="' + encodeURIComponent(fileName) + '"', 'attachment; filename="' + encodeURIComponent(fileName) + '"',
}); });
res.send(rawContent); res.send(zipFileBuffer);
} }
@UseGuards(JwtAuthGuard) @UseGuards(JwtAuthGuard)
@@ -89,10 +89,28 @@ export class ExportService {
return; return;
} }
async exportPageWithChildren(pageId: string, format: string) { async exportPages(
const pages = await this.pageRepo.getPageAndDescendants(pageId, { pageId: string,
includeContent: true, format: string,
}); includeAttachments: boolean,
includeChildren: boolean,
) {
let pages: Page[];
if (includeChildren) {
//@ts-ignore
pages = await this.pageRepo.getPageAndDescendants(pageId, {
includeContent: true,
});
} else {
// Only fetch the single page when includeChildren is false
const page = await this.pageRepo.findById(pageId, {
includeContent: true,
});
if (page){
pages = [page];
}
}
if (!pages || pages.length === 0) { if (!pages || pages.length === 0) {
throw new BadRequestException('No pages to export'); throw new BadRequestException('No pages to export');
@@ -105,7 +123,7 @@ export class ExportService {
const tree = buildTree(pages as Page[]); const tree = buildTree(pages as Page[]);
const zip = new JSZip(); const zip = new JSZip();
await this.zipPages(tree, format, zip); await this.zipPages(tree, format, zip, includeAttachments);
const zipFile = zip.generateNodeStream({ const zipFile = zip.generateNodeStream({
type: 'nodebuffer', type: 'nodebuffer',
@@ -168,7 +186,7 @@ export class ExportService {
tree: PageExportTree, tree: PageExportTree,
format: string, format: string,
zip: JSZip, zip: JSZip,
includeAttachments = true, includeAttachments: boolean,
): Promise<void> { ): Promise<void> {
const slugIdToPath: Record<string, string> = {}; const slugIdToPath: Record<string, string> = {};
@@ -200,7 +218,8 @@ export class ExportService {
if (includeAttachments) { if (includeAttachments) {
await this.zipAttachments(updatedJsonContent, page.spaceId, folder); await this.zipAttachments(updatedJsonContent, page.spaceId, folder);
updatedJsonContent = updateAttachmentUrlsToLocalPaths(updatedJsonContent); updatedJsonContent =
updateAttachmentUrlsToLocalPaths(updatedJsonContent);
} }
const pageTitle = getPageTitle(page.title); const pageTitle = getPageTitle(page.title);
@@ -70,7 +70,16 @@ function taskList(turndownService: TurndownService) {
) as HTMLInputElement; ) as HTMLInputElement;
const isChecked = checkbox.checked; const isChecked = checkbox.checked;
return `- ${isChecked ? '[x]' : '[ ]'} ${content.trim()} \n`; // Process content like regular list items
content = content
.replace(/^\n+/, '') // remove leading newlines
.replace(/\n+$/, '\n') // replace trailing newlines with just a single one
.replace(/\n/gm, '\n '); // indent nested content with 2 spaces
// Create the checkbox prefix
const prefix = `- ${isChecked ? '[x]' : '[ ]'} `;
return prefix + content + (node.nextSibling && !/\n$/.test(content) ? '\n' : '');
}, },
}); });
} }
@@ -178,7 +178,7 @@ export class ImportService {
.selectFrom('pages') .selectFrom('pages')
.select(['id', 'position']) .select(['id', 'position'])
.where('spaceId', '=', spaceId) .where('spaceId', '=', spaceId)
.orderBy('position', 'desc') .orderBy('position', (ob) => ob.collate('C').desc())
.limit(1) .limit(1)
.where('parentPageId', 'is', null) .where('parentPageId', 'is', null)
.executeTakeFirst(); .executeTakeFirst();
@@ -40,8 +40,11 @@ export class LocalDriver implements StorageDriver {
async copy(fromFilePath: string, toFilePath: string): Promise<void> { async copy(fromFilePath: string, toFilePath: string): Promise<void> {
try { try {
const fromFullPath = this._fullPath(fromFilePath);
const toFullPath = this._fullPath(toFilePath);
if (await this.exists(fromFilePath)) { if (await this.exists(fromFilePath)) {
await fs.copy(fromFilePath, toFilePath); await fs.copy(fromFullPath, toFullPath);
} }
} catch (err) { } catch (err) {
throw new Error(`Failed to copy file: ${(err as Error).message}`); throw new Error(`Failed to copy file: ${(err as Error).message}`);
+20
View File
@@ -543,6 +543,9 @@ importers:
nodemailer: nodemailer:
specifier: ^7.0.3 specifier: ^7.0.3
version: 7.0.3 version: 7.0.3
openai:
specifier: ^5.12.2
version: 5.12.2(ws@8.18.2)(zod@3.25.56)
openid-client: openid-client:
specifier: ^5.7.1 specifier: ^5.7.1
version: 5.7.1 version: 5.7.1
@@ -7655,6 +7658,18 @@ packages:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==} resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
openai@5.12.2:
resolution: {integrity: sha512-xqzHHQch5Tws5PcKR2xsZGX9xtch+JQFz5zb14dGqlshmmDAFBFEWmeIpf7wVqWV+w7Emj7jRgkNJakyKE0tYQ==}
hasBin: true
peerDependencies:
ws: ^8.18.0
zod: ^3.23.8
peerDependenciesMeta:
ws:
optional: true
zod:
optional: true
openid-client@5.7.1: openid-client@5.7.1:
resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==}
@@ -18262,6 +18277,11 @@ snapshots:
is-docker: 2.2.1 is-docker: 2.2.1
is-wsl: 2.2.0 is-wsl: 2.2.0
openai@5.12.2(ws@8.18.2)(zod@3.25.56):
optionalDependencies:
ws: 8.18.2
zod: 3.25.56
openid-client@5.7.1: openid-client@5.7.1:
dependencies: dependencies:
jose: 4.15.9 jose: 4.15.9