@lobehub/lobehub 2.0.0-next.257 → 2.0.0-next.259

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 (31) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/apps/desktop/{electron-builder.js → electron-builder.mjs} +24 -11
  3. package/apps/desktop/electron.vite.config.ts +10 -4
  4. package/apps/desktop/native-deps.config.mjs +102 -0
  5. package/apps/desktop/package.json +8 -7
  6. package/apps/desktop/src/main/__mocks__/node-mac-permissions.ts +21 -0
  7. package/apps/desktop/src/main/__mocks__/setup.ts +8 -0
  8. package/apps/desktop/src/main/controllers/SystemCtr.ts +20 -159
  9. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +58 -90
  10. package/apps/desktop/src/main/utils/permissions.ts +307 -0
  11. package/apps/desktop/tsconfig.json +2 -1
  12. package/apps/desktop/vitest.config.mts +1 -0
  13. package/changelog/v1.json +18 -0
  14. package/locales/en-US/setting.json +1 -0
  15. package/locales/zh-CN/setting.json +1 -0
  16. package/package.json +1 -1
  17. package/packages/builtin-tool-memory/package.json +2 -1
  18. package/packages/builtin-tool-memory/src/executor/index.ts +2 -30
  19. package/packages/database/src/schemas/agentCronJob.ts +53 -19
  20. package/packages/file-loaders/package.json +1 -1
  21. package/packages/prompts/src/prompts/userMemory/__snapshots__/formatSearchResults.test.ts.snap +65 -0
  22. package/packages/prompts/src/prompts/userMemory/formatSearchResults.test.ts +200 -0
  23. package/packages/prompts/src/prompts/userMemory/formatSearchResults.ts +164 -0
  24. package/packages/prompts/src/prompts/userMemory/index.ts +2 -0
  25. package/scripts/electronWorkflow/buildNextApp.mts +39 -1
  26. package/scripts/electronWorkflow/modifiers/nextConfig.mts +26 -0
  27. package/src/app/[variants]/(main)/chat/cron/[cronId]/index.tsx +12 -8
  28. package/src/app/[variants]/(main)/chat/profile/features/AgentCronJobs/CronJobForm.tsx +9 -7
  29. package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +0 -9
  30. package/src/features/PageEditor/Copilot/index.tsx +0 -1
  31. package/apps/desktop/src/main/utils/fullDiskAccess.ts +0 -121
@@ -6,6 +6,7 @@ import type {
6
6
  RemoveIdentityActionSchema,
7
7
  UpdateIdentityActionSchema,
8
8
  } from '@lobechat/memory-user-memory/schemas';
9
+ import { formatMemorySearchResults } from '@lobechat/prompts';
9
10
  import { BaseExecutor, type BuiltinToolResult, SearchMemoryParams } from '@lobechat/types';
10
11
  import type { z } from 'zod';
11
12
 
@@ -14,35 +15,6 @@ import { userMemoryService } from '@/services/userMemory';
14
15
  import { MemoryIdentifier } from '../manifest';
15
16
  import { MemoryApiName } from '../types';
16
17
 
17
- /**
18
- * Format search results into human-readable summary
19
- */
20
- const formatSearchResultsSummary = (result: {
21
- contexts: unknown[];
22
- experiences: unknown[];
23
- preferences: unknown[];
24
- }): string => {
25
- const total = result.contexts.length + result.experiences.length + result.preferences.length;
26
-
27
- if (total === 0) {
28
- return '🔍 No memories found matching the query.';
29
- }
30
-
31
- const parts: string[] = [`🔍 Found ${total} memories:`];
32
-
33
- if (result.contexts.length > 0) {
34
- parts.push(`- ${result.contexts.length} context memories`);
35
- }
36
- if (result.experiences.length > 0) {
37
- parts.push(`- ${result.experiences.length} experience memories`);
38
- }
39
- if (result.preferences.length > 0) {
40
- parts.push(`- ${result.preferences.length} preference memories`);
41
- }
42
-
43
- return parts.join('\n');
44
- };
45
-
46
18
  /**
47
19
  * Memory Tool Executor
48
20
  *
@@ -62,7 +34,7 @@ class MemoryExecutor extends BaseExecutor<typeof MemoryApiName> {
62
34
  const result = await userMemoryService.searchMemory(params);
63
35
 
64
36
  return {
65
- content: formatSearchResultsSummary(result),
37
+ content: formatMemorySearchResults({ query: params.query, results: result }),
66
38
  state: result,
67
39
  success: true,
68
40
  };
@@ -43,7 +43,7 @@ export const agentCronJobs = pgTable(
43
43
 
44
44
  // Core configuration
45
45
  enabled: boolean('enabled').default(true),
46
- cronPattern: text('cron_pattern').notNull(), // e.g., "0 */30 * * *"
46
+ cronPattern: text('cron_pattern').notNull(), // e.g., "*/30 * * * *" (every 30 minutes)
47
47
  timezone: text('timezone').default('UTC'),
48
48
 
49
49
  // Content fields
@@ -82,27 +82,61 @@ export const cronPatternSchema = z
82
82
  'Invalid cron pattern',
83
83
  );
84
84
 
85
- // Minimum 30 minutes validation
85
+ // Minimum 30 minutes validation (using standard cron format)
86
86
  export const minimumIntervalSchema = z.string().refine((pattern) => {
87
- // For simplicity, we'll validate common patterns
88
- // More complex validation can be added later
89
- const thirtyMinPatterns = [
90
- '0 */30 * * *', // Every 30 minutes
91
- '0 0 * * *', // Every hour
92
- '0 0 */2 * *', // Every 2 hours
93
- '0 0 */6 * *', // Every 6 hours
94
- '0 0 0 * *', // Daily
95
- '0 0 0 * * 1', // Weekly
96
- '0 0 0 1 *', // Monthly
87
+ // Standard cron format: minute hour day month weekday
88
+ const allowedPatterns = [
89
+ '*/30 * * * *', // Every 30 minutes
90
+ '0 * * * *', // Every hour
91
+ '0 */2 * * *', // Every 2 hours
92
+ '0 */3 * * *', // Every 3 hours
93
+ '0 */4 * * *', // Every 4 hours
94
+ '0 */6 * * *', // Every 6 hours
95
+ '0 */8 * * *', // Every 8 hours
96
+ '0 */12 * * *', // Every 12 hours
97
+ '0 0 * * *', // Daily at midnight
98
+ '0 0 * * 0', // Weekly on Sunday
99
+ '0 0 1 * *', // Monthly on 1st
97
100
  ];
98
101
 
99
- // Check if it matches allowed patterns or follows 30+ minute intervals
100
- return (
101
- thirtyMinPatterns.includes(pattern) ||
102
- pattern.includes('*/30') ||
103
- pattern.includes('*/60') ||
104
- /0 \d+ \* \* \*/.test(pattern)
105
- ); // Hours pattern
102
+ // Check if it matches allowed patterns
103
+ if (allowedPatterns.includes(pattern)) {
104
+ return true;
105
+ }
106
+
107
+ // Parse pattern to validate minimum 30-minute interval
108
+ const parts = pattern.split(' ');
109
+ if (parts.length !== 5) {
110
+ return false;
111
+ }
112
+
113
+ const [minute, hour] = parts;
114
+
115
+ // Allow minute intervals >= 30 (e.g., */30, */45, */60)
116
+ if (minute.startsWith('*/')) {
117
+ const interval = parseInt(minute.slice(2));
118
+ if (!isNaN(interval) && interval >= 30) {
119
+ return true;
120
+ }
121
+ }
122
+
123
+ // Allow hourly patterns: 0 */N * * * where N >= 1
124
+ if (minute === '0' && hour.startsWith('*/')) {
125
+ const interval = parseInt(hour.slice(2));
126
+ if (!isNaN(interval) && interval >= 1) {
127
+ return true;
128
+ }
129
+ }
130
+
131
+ // Allow specific hour patterns: 0 N * * * (runs once per day)
132
+ if (minute === '0' && /^\d+$/.test(hour)) {
133
+ const h = parseInt(hour);
134
+ if (!isNaN(h) && h >= 0 && h <= 23) {
135
+ return true;
136
+ }
137
+ }
138
+
139
+ return false;
106
140
  }, 'Minimum execution interval is 30 minutes');
107
141
 
108
142
  export const executionConditionsSchema = z
@@ -25,7 +25,6 @@
25
25
  "test:coverage": "vitest --coverage --silent='passed-only'"
26
26
  },
27
27
  "dependencies": {
28
- "@napi-rs/canvas": "^0.1.70",
29
28
  "@xmldom/xmldom": "^0.9.8",
30
29
  "concat-stream": "^2.0.0",
31
30
  "debug": "^4.4.3",
@@ -37,6 +36,7 @@
37
36
  "yauzl": "^3.2.0"
38
37
  },
39
38
  "devDependencies": {
39
+ "@napi-rs/canvas": "^0.1.70",
40
40
  "@types/concat-stream": "^2.0.3",
41
41
  "@types/yauzl": "^2.10.3",
42
42
  "typescript": "^5.9.3"
@@ -0,0 +1,65 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`formatMemorySearchResults > should format context memories with full content 1`] = `
4
+ "<memories query="project" total="1">
5
+ <contexts count="1">
6
+ <context id="ctx-1" title="Web App Project" urgency=7 impact=8 type="project" status="in-progress">
7
+ Building a new web application
8
+ <subjects>John (person)</subjects>
9
+ <objects>React (application)</objects>
10
+ </context>
11
+ </contexts>
12
+ </memories>"
13
+ `;
14
+
15
+ exports[`formatMemorySearchResults > should format experience memories with full content 1`] = `
16
+ "<memories query="debugging" total="1">
17
+ <experiences count="1">
18
+ <experience id="exp-1" type="lesson" confidence=9>
19
+ <situation>Debugging complex state issues</situation>
20
+ <keyLearning>Breakpoints save time in complex debugging</keyLearning>
21
+ </experience>
22
+ </experiences>
23
+ </memories>"
24
+ `;
25
+
26
+ exports[`formatMemorySearchResults > should format mixed results with all memory types 1`] = `
27
+ "<memories query="work" total="3">
28
+ <contexts count="1">
29
+ <context id="ctx-1" title="Context Title">
30
+ Context description
31
+ </context>
32
+ </contexts>
33
+ <experiences count="1">
34
+ <experience id="exp-1">
35
+ <situation>Situation</situation>
36
+ <keyLearning>Key learning</keyLearning>
37
+ </experience>
38
+ </experiences>
39
+ <preferences count="1">
40
+ <preference id="pref-1">Directive</preference>
41
+ </preferences>
42
+ </memories>"
43
+ `;
44
+
45
+ exports[`formatMemorySearchResults > should format preference memories with full content 1`] = `
46
+ "<memories query="code style" total="1">
47
+ <preferences count="1">
48
+ <preference id="pref-1" type="coding-standard" priority=10>Always use TypeScript strict mode</preference>
49
+ </preferences>
50
+ </memories>"
51
+ `;
52
+
53
+ exports[`formatMemorySearchResults > should handle null and undefined values gracefully 1`] = `
54
+ "<memories query="test" total="1">
55
+ <contexts count="1">
56
+ <context id="ctx-1"></context>
57
+ </contexts>
58
+ </memories>"
59
+ `;
60
+
61
+ exports[`formatMemorySearchResults > should return empty results message when no memories found 1`] = `
62
+ "<memories query="test query">
63
+ <status>No memories found matching the query.</status>
64
+ </memories>"
65
+ `;
@@ -0,0 +1,200 @@
1
+ import { UserMemoryContextObjectType, UserMemoryContextSubjectType } from '@lobechat/types';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ import { formatMemorySearchResults } from './formatSearchResults';
5
+
6
+ describe('formatMemorySearchResults', () => {
7
+ it('should return empty results message when no memories found', () => {
8
+ const result = formatMemorySearchResults({
9
+ query: 'test query',
10
+ results: {
11
+ contexts: [],
12
+ experiences: [],
13
+ preferences: [],
14
+ },
15
+ });
16
+
17
+ expect(result).toMatchSnapshot();
18
+ });
19
+
20
+ it('should format context memories with full content', () => {
21
+ const result = formatMemorySearchResults({
22
+ query: 'project',
23
+ results: {
24
+ contexts: [
25
+ {
26
+ accessedAt: new Date('2024-01-01'),
27
+ associatedObjects: [{ name: 'React', type: UserMemoryContextObjectType.Application }],
28
+ associatedSubjects: [{ name: 'John', type: UserMemoryContextSubjectType.Person }],
29
+ createdAt: new Date('2024-01-01'),
30
+ currentStatus: 'in-progress',
31
+ description: 'Building a new web application',
32
+ id: 'ctx-1',
33
+ metadata: null,
34
+ scoreImpact: 8,
35
+ scoreUrgency: 7,
36
+ tags: ['frontend', 'react'],
37
+ title: 'Web App Project',
38
+ type: 'project',
39
+ updatedAt: new Date('2024-01-01'),
40
+ userMemoryIds: null,
41
+ },
42
+ ],
43
+ experiences: [],
44
+ preferences: [],
45
+ },
46
+ });
47
+
48
+ expect(result).toMatchSnapshot();
49
+ });
50
+
51
+ it('should format experience memories with full content', () => {
52
+ const result = formatMemorySearchResults({
53
+ query: 'debugging',
54
+ results: {
55
+ contexts: [],
56
+ experiences: [
57
+ {
58
+ accessedAt: new Date('2024-01-01'),
59
+ action: 'Used breakpoints instead of console.log',
60
+ createdAt: new Date('2024-01-01'),
61
+ id: 'exp-1',
62
+ keyLearning: 'Breakpoints save time in complex debugging',
63
+ metadata: null,
64
+ possibleOutcome: 'Faster debugging sessions',
65
+ reasoning: 'Console logs clutter the code',
66
+ scoreConfidence: 9,
67
+ situation: 'Debugging complex state issues',
68
+ tags: ['debugging', 'best-practice'],
69
+ type: 'lesson',
70
+ updatedAt: new Date('2024-01-01'),
71
+ userMemoryId: null,
72
+ },
73
+ ],
74
+ preferences: [],
75
+ },
76
+ });
77
+
78
+ expect(result).toMatchSnapshot();
79
+ });
80
+
81
+ it('should format preference memories with full content', () => {
82
+ const result = formatMemorySearchResults({
83
+ query: 'code style',
84
+ results: {
85
+ contexts: [],
86
+ experiences: [],
87
+ preferences: [
88
+ {
89
+ accessedAt: new Date('2024-01-01'),
90
+ conclusionDirectives: 'Always use TypeScript strict mode',
91
+ createdAt: new Date('2024-01-01'),
92
+ id: 'pref-1',
93
+ metadata: null,
94
+ scorePriority: 10,
95
+ suggestions: 'Consider adding eslint rules',
96
+ tags: ['typescript', 'code-quality'],
97
+ type: 'coding-standard',
98
+ updatedAt: new Date('2024-01-01'),
99
+ userMemoryId: null,
100
+ },
101
+ ],
102
+ },
103
+ });
104
+
105
+ expect(result).toMatchSnapshot();
106
+ });
107
+
108
+ it('should format mixed results with all memory types', () => {
109
+ const result = formatMemorySearchResults({
110
+ query: 'work',
111
+ results: {
112
+ contexts: [
113
+ {
114
+ accessedAt: new Date('2024-01-01'),
115
+ associatedObjects: null,
116
+ associatedSubjects: null,
117
+ createdAt: new Date('2024-01-01'),
118
+ currentStatus: null,
119
+ description: 'Context description',
120
+ id: 'ctx-1',
121
+ metadata: null,
122
+ scoreImpact: null,
123
+ scoreUrgency: null,
124
+ tags: null,
125
+ title: 'Context Title',
126
+ type: null,
127
+ updatedAt: new Date('2024-01-01'),
128
+ userMemoryIds: null,
129
+ },
130
+ ],
131
+ experiences: [
132
+ {
133
+ accessedAt: new Date('2024-01-01'),
134
+ action: null,
135
+ createdAt: new Date('2024-01-01'),
136
+ id: 'exp-1',
137
+ keyLearning: 'Key learning',
138
+ metadata: null,
139
+ possibleOutcome: null,
140
+ reasoning: null,
141
+ scoreConfidence: null,
142
+ situation: 'Situation',
143
+ tags: null,
144
+ type: null,
145
+ updatedAt: new Date('2024-01-01'),
146
+ userMemoryId: null,
147
+ },
148
+ ],
149
+ preferences: [
150
+ {
151
+ accessedAt: new Date('2024-01-01'),
152
+ conclusionDirectives: 'Directive',
153
+ createdAt: new Date('2024-01-01'),
154
+ id: 'pref-1',
155
+ metadata: null,
156
+ scorePriority: null,
157
+ suggestions: null,
158
+ tags: null,
159
+ type: null,
160
+ updatedAt: new Date('2024-01-01'),
161
+ userMemoryId: null,
162
+ },
163
+ ],
164
+ },
165
+ });
166
+
167
+ expect(result).toMatchSnapshot();
168
+ });
169
+
170
+ it('should handle null and undefined values gracefully', () => {
171
+ const result = formatMemorySearchResults({
172
+ query: 'test',
173
+ results: {
174
+ contexts: [
175
+ {
176
+ accessedAt: new Date('2024-01-01'),
177
+ associatedObjects: null,
178
+ associatedSubjects: null,
179
+ createdAt: new Date('2024-01-01'),
180
+ currentStatus: null,
181
+ description: null,
182
+ id: 'ctx-1',
183
+ metadata: null,
184
+ scoreImpact: null,
185
+ scoreUrgency: null,
186
+ tags: null,
187
+ title: null,
188
+ type: null,
189
+ updatedAt: new Date('2024-01-01'),
190
+ userMemoryIds: null,
191
+ },
192
+ ],
193
+ experiences: [],
194
+ preferences: [],
195
+ },
196
+ });
197
+
198
+ expect(result).toMatchSnapshot();
199
+ });
200
+ });
@@ -0,0 +1,164 @@
1
+ import type { SearchMemoryResult } from '@lobechat/types';
2
+
3
+ /**
4
+ * Search result item interfaces matching the SearchMemoryResult type
5
+ */
6
+ type ContextResult = SearchMemoryResult['contexts'][number];
7
+ type ExperienceResult = SearchMemoryResult['experiences'][number];
8
+ type PreferenceResult = SearchMemoryResult['preferences'][number];
9
+
10
+ /**
11
+ * Format a single context memory item for search results
12
+ * Format: attributes for metadata, description as text content
13
+ */
14
+ const formatContextResult = (item: ContextResult): string => {
15
+ const attrs: string[] = [`id="${item.id}"`];
16
+
17
+ if (item.title) {
18
+ attrs.push(`title="${item.title}"`);
19
+ }
20
+ if (item.scoreUrgency !== null && item.scoreUrgency !== undefined) {
21
+ attrs.push(`urgency=${item.scoreUrgency}`);
22
+ }
23
+ if (item.scoreImpact !== null && item.scoreImpact !== undefined) {
24
+ attrs.push(`impact=${item.scoreImpact}`);
25
+ }
26
+ if (item.type) {
27
+ attrs.push(`type="${item.type}"`);
28
+ }
29
+ if (item.currentStatus) {
30
+ attrs.push(`status="${item.currentStatus}"`);
31
+ }
32
+
33
+ const children: string[] = [];
34
+
35
+ // Description as main text content
36
+ if (item.description) {
37
+ children.push(` ${item.description}`);
38
+ }
39
+
40
+ // Associated subjects (actors)
41
+ if (item.associatedSubjects && item.associatedSubjects.length > 0) {
42
+ const subjects = item.associatedSubjects
43
+ .filter((s) => s?.name)
44
+ .map((s) => `${s.name}${s.type ? ` (${s.type})` : ''}`)
45
+ .join(', ');
46
+ if (subjects) {
47
+ children.push(` <subjects>${subjects}</subjects>`);
48
+ }
49
+ }
50
+
51
+ // Associated objects (resources)
52
+ if (item.associatedObjects && item.associatedObjects.length > 0) {
53
+ const objects = item.associatedObjects
54
+ .filter((o) => o?.name)
55
+ .map((o) => `${o.name}${o.type ? ` (${o.type})` : ''}`)
56
+ .join(', ');
57
+ if (objects) {
58
+ children.push(` <objects>${objects}</objects>`);
59
+ }
60
+ }
61
+
62
+ const content = children.length > 0 ? `\n${children.join('\n')}\n ` : '';
63
+
64
+ return ` <context ${attrs.join(' ')}>${content}</context>`;
65
+ };
66
+
67
+ /**
68
+ * Format a single experience memory item for search results
69
+ * Format: attributes for metadata, situation and keyLearning as child elements
70
+ */
71
+ const formatExperienceResult = (item: ExperienceResult): string => {
72
+ const attrs: string[] = [`id="${item.id}"`];
73
+
74
+ if (item.type) {
75
+ attrs.push(`type="${item.type}"`);
76
+ }
77
+ if (item.scoreConfidence !== null && item.scoreConfidence !== undefined) {
78
+ attrs.push(`confidence=${item.scoreConfidence}`);
79
+ }
80
+
81
+ const children: string[] = [];
82
+
83
+ if (item.situation) {
84
+ children.push(` <situation>${item.situation}</situation>`);
85
+ }
86
+ if (item.keyLearning) {
87
+ children.push(` <keyLearning>${item.keyLearning}</keyLearning>`);
88
+ }
89
+
90
+ const content = children.length > 0 ? `\n${children.join('\n')}\n ` : '';
91
+
92
+ return ` <experience ${attrs.join(' ')}>${content}</experience>`;
93
+ };
94
+
95
+ /**
96
+ * Format a single preference memory item for search results
97
+ * Format: attributes for metadata, directives as text content
98
+ */
99
+ const formatPreferenceResult = (item: PreferenceResult): string => {
100
+ const attrs: string[] = [`id="${item.id}"`];
101
+
102
+ if (item.type) {
103
+ attrs.push(`type="${item.type}"`);
104
+ }
105
+ if (item.scorePriority !== null && item.scorePriority !== undefined) {
106
+ attrs.push(`priority=${item.scorePriority}`);
107
+ }
108
+
109
+ const content = item.conclusionDirectives || '';
110
+
111
+ return ` <preference ${attrs.join(' ')}>${content}</preference>`;
112
+ };
113
+
114
+ export interface FormatSearchResultsOptions {
115
+ /** The search query that was used */
116
+ query: string;
117
+ /** The search results to format */
118
+ results: SearchMemoryResult;
119
+ }
120
+
121
+ /**
122
+ * Format memory search results as XML for LLM consumption.
123
+ *
124
+ * This function formats the complete search results with all content details,
125
+ * making the retrieved memories directly usable by the LLM for reasoning
126
+ * and response generation.
127
+ */
128
+ export const formatMemorySearchResults = ({
129
+ query,
130
+ results,
131
+ }: FormatSearchResultsOptions): string => {
132
+ const { contexts, experiences, preferences } = results;
133
+ const total = contexts.length + experiences.length + preferences.length;
134
+
135
+ if (total === 0) {
136
+ return `<memories query="${query}">
137
+ <status>No memories found matching the query.</status>
138
+ </memories>`;
139
+ }
140
+
141
+ const sections: string[] = [];
142
+
143
+ // Add contexts section
144
+ if (contexts.length > 0) {
145
+ const contextsXml = contexts.map(formatContextResult).join('\n');
146
+ sections.push(`<contexts count="${contexts.length}">\n${contextsXml}\n</contexts>`);
147
+ }
148
+
149
+ // Add experiences section
150
+ if (experiences.length > 0) {
151
+ const experiencesXml = experiences.map(formatExperienceResult).join('\n');
152
+ sections.push(`<experiences count="${experiences.length}">\n${experiencesXml}\n</experiences>`);
153
+ }
154
+
155
+ // Add preferences section
156
+ if (preferences.length > 0) {
157
+ const preferencesXml = preferences.map(formatPreferenceResult).join('\n');
158
+ sections.push(`<preferences count="${preferences.length}">\n${preferencesXml}\n</preferences>`);
159
+ }
160
+
161
+ return `<memories query="${query}" total="${total}">
162
+ ${sections.join('\n')}
163
+ </memories>`;
164
+ };
@@ -1,3 +1,5 @@
1
+ export * from './formatSearchResults';
2
+
1
3
  /**
2
4
  * User memory item interfaces
3
5
  */
@@ -20,6 +20,31 @@ const foldersToSymlink = [
20
20
 
21
21
  const foldersToCopy = ['src', 'scripts'];
22
22
 
23
+ // Assets to remove from desktop build output (not needed for Electron app)
24
+ const assetsToRemove = [
25
+ // Icons & favicons
26
+ 'apple-touch-icon.png',
27
+ 'favicon.ico',
28
+ 'favicon-32x32.ico',
29
+ 'favicon-16x16.png',
30
+ 'favicon-32x32.png',
31
+
32
+ // SEO & sitemap
33
+ 'sitemap.xml',
34
+ 'sitemap-index.xml',
35
+ 'sitemap',
36
+ 'robots.txt',
37
+
38
+ // Incompatible pages
39
+ 'not-compatible.html',
40
+ 'not-compatible',
41
+
42
+ // Large media assets
43
+ 'videos',
44
+ 'screenshots',
45
+ 'og',
46
+ ];
47
+
23
48
  const filesToCopy = [
24
49
  'package.json',
25
50
  'tsconfig.json',
@@ -85,10 +110,13 @@ const build = async () => {
85
110
 
86
111
  console.log('🏗 Running next build in shadow workspace...');
87
112
  try {
88
- execSync('next build --webpack', {
113
+ execSync('next build', {
89
114
  cwd: TEMP_DIR,
90
115
  env: {
91
116
  ...process.env,
117
+ // Pass PROJECT_ROOT to next.config.ts for outputFileTracingRoot
118
+ // This fixes Turbopack symlink resolution when building in shadow workspace
119
+ ELECTRON_BUILD_PROJECT_ROOT: PROJECT_ROOT,
92
120
  NODE_OPTIONS: process.env.NODE_OPTIONS || '--max-old-space-size=8192',
93
121
  },
94
122
  stdio: 'inherit',
@@ -106,6 +134,16 @@ const build = async () => {
106
134
  if (fs.existsSync(sourceOutDir)) {
107
135
  console.log('📦 Moving "out" directory...');
108
136
  await fs.move(sourceOutDir, targetOutDir);
137
+
138
+ // Remove unnecessary assets from desktop build
139
+ console.log('🗑️ Removing unnecessary assets...');
140
+ for (const asset of assetsToRemove) {
141
+ const assetPath = path.join(targetOutDir, asset);
142
+ if (fs.existsSync(assetPath)) {
143
+ await fs.remove(assetPath);
144
+ console.log(` Removed: ${asset}`);
145
+ }
146
+ }
109
147
  } else {
110
148
  console.warn("⚠️ 'out' directory not found. Using '.next' instead (fallback)?");
111
149
  const sourceNextDir = path.join(TEMP_DIR, '.next');
@@ -144,6 +144,32 @@ export const modifyNextConfig = async (TEMP_DIR: string) => {
144
144
  }
145
145
  }
146
146
 
147
+ // 6. Inject outputFileTracingRoot to fix symlink resolution for Turbopack
148
+ // When building in shadow workspace (TEMP_DIR), symlinks (e.g., node_modules) point to PROJECT_ROOT
149
+ // Turbopack's root defaults to TEMP_DIR, causing strip_prefix to fail for paths outside TEMP_DIR
150
+ // Setting outputFileTracingRoot to PROJECT_ROOT allows Turbopack to correctly resolve these symlinks
151
+ // We use ELECTRON_BUILD_PROJECT_ROOT env var which is set by buildNextApp.mts
152
+ const outputFileTracingRootPair = nextConfigDecl.find({
153
+ rule: {
154
+ pattern: 'outputFileTracingRoot: $A',
155
+ },
156
+ });
157
+ if (!outputFileTracingRootPair) {
158
+ const objectNode = nextConfigDecl.find({
159
+ rule: { kind: 'object' },
160
+ });
161
+ if (objectNode) {
162
+ const range = objectNode.range();
163
+ // Insert outputFileTracingRoot that reads from env var at build time
164
+ // Falls back to current directory if not in electron build context
165
+ edits.push({
166
+ end: range.start.index + 1,
167
+ start: range.start.index + 1,
168
+ text: "\n outputFileTracingRoot: process.env.ELECTRON_BUILD_PROJECT_ROOT || process.cwd(),",
169
+ });
170
+ }
171
+ }
172
+
147
173
  // Remove withPWA wrapper
148
174
  const withPWA = root.find({
149
175
  rule: {