synthos 0.10.1 → 0.11.0
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/README.md +5 -5
- package/default-pages/elevenlabs_effects_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_effects_studio/page.html +1345 -1363
- package/default-pages/elevenlabs_effects_studio/page.json +13 -11
- package/default-pages/elevenlabs_voice_studio/chat-history.json +1 -0
- package/default-pages/elevenlabs_voice_studio/page.html +782 -801
- package/default-pages/elevenlabs_voice_studio/page.json +13 -11
- package/default-pages/json_tools/chat-history.json +1 -0
- package/default-pages/json_tools/page.html +70 -90
- package/default-pages/json_tools/page.json +12 -10
- package/default-pages/my_notes/chat-history.json +1 -0
- package/default-pages/my_notes/page.html +115 -131
- package/default-pages/my_notes/page.json +14 -12
- package/default-pages/neon_asteroids/chat-history.json +1 -0
- package/default-pages/neon_asteroids/page.html +1777 -1803
- package/default-pages/neon_asteroids/page.json +14 -12
- package/default-pages/oregon_trail/chat-history.json +1 -0
- package/default-pages/oregon_trail/page.html +290 -307
- package/default-pages/oregon_trail/page.json +14 -12
- package/default-pages/solar_explorer/chat-history.json +1 -0
- package/default-pages/solar_explorer/page.html +1929 -1951
- package/default-pages/solar_explorer/page.json +14 -12
- package/default-pages/solar_tutorial/chat-history.json +1 -0
- package/default-pages/solar_tutorial/page.html +464 -478
- package/default-pages/solar_tutorial/page.json +12 -10
- package/default-pages/us_map/chat-history.json +1 -0
- package/default-pages/us_map/page.html +170 -193
- package/default-pages/us_map/page.json +14 -12
- package/default-pages/us_map/page.light.png +0 -0
- package/default-pages/us_map_1850/chat-history.json +1 -0
- package/default-pages/us_map_1850/page.html +302 -326
- package/default-pages/us_map_1850/page.json +14 -12
- package/default-pages/western_cities_1850/chat-history.json +1 -0
- package/default-pages/western_cities_1850/page.html +503 -527
- package/default-pages/western_cities_1850/page.json +14 -12
- package/default-themes/aurora-dawn.v3.css +15 -14
- package/default-themes/aurora-dusk.v3.css +26 -26
- package/default-themes/cosmos-dawn.v3.css +15 -14
- package/default-themes/cosmos-dusk.v3.css +26 -26
- package/default-themes/elemental-dawn.v3.css +200 -0
- package/default-themes/nebula-dawn.v3.css +15 -14
- package/default-themes/nebula-dusk.v3.css +24 -24
- package/default-themes/solar-flare-dawn.v3.css +15 -14
- package/default-themes/solar-flare-dusk.v3.css +26 -26
- package/dist/builders/anthropic.d.ts +26 -2
- package/dist/builders/anthropic.d.ts.map +1 -1
- package/dist/builders/anthropic.js +132 -31
- package/dist/builders/anthropic.js.map +1 -1
- package/dist/builders/claudecode.d.ts +13 -0
- package/dist/builders/claudecode.d.ts.map +1 -0
- package/dist/builders/claudecode.js +253 -0
- package/dist/builders/claudecode.js.map +1 -0
- package/dist/builders/index.d.ts +2 -1
- package/dist/builders/index.d.ts.map +1 -1
- package/dist/builders/index.js +8 -1
- package/dist/builders/index.js.map +1 -1
- package/dist/builders/openai.js +2 -1
- package/dist/builders/openai.js.map +1 -1
- package/dist/builders/types.d.ts +31 -7
- package/dist/builders/types.d.ts.map +1 -1
- package/dist/builders/types.js +60 -28
- package/dist/builders/types.js.map +1 -1
- package/dist/connectors/types.d.ts +8 -0
- package/dist/connectors/types.d.ts.map +1 -1
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +13 -6
- package/dist/init.js.map +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +161 -14
- package/dist/migrations.js.map +1 -1
- package/dist/models/anthropic.d.ts +1 -0
- package/dist/models/anthropic.d.ts.map +1 -1
- package/dist/models/anthropic.js +129 -29
- package/dist/models/anthropic.js.map +1 -1
- package/dist/models/chainOfThought.d.ts.map +1 -1
- package/dist/models/chainOfThought.js +32 -19
- package/dist/models/chainOfThought.js.map +1 -1
- package/dist/models/index.d.ts +2 -2
- package/dist/models/index.d.ts.map +1 -1
- package/dist/models/index.js +2 -1
- package/dist/models/index.js.map +1 -1
- package/dist/models/providers.d.ts +1 -0
- package/dist/models/providers.d.ts.map +1 -1
- package/dist/models/providers.js +12 -4
- package/dist/models/providers.js.map +1 -1
- package/dist/models/types.d.ts +15 -1
- package/dist/models/types.d.ts.map +1 -1
- package/dist/models/types.js.map +1 -1
- package/dist/pages.d.ts +57 -8
- package/dist/pages.d.ts.map +1 -1
- package/dist/pages.js +258 -45
- package/dist/pages.js.map +1 -1
- package/dist/service/createCompletePrompt.d.ts.map +1 -1
- package/dist/service/createCompletePrompt.js +5 -0
- package/dist/service/createCompletePrompt.js.map +1 -1
- package/dist/service/mediaCache.d.ts +36 -0
- package/dist/service/mediaCache.d.ts.map +1 -0
- package/dist/service/mediaCache.js +182 -0
- package/dist/service/mediaCache.js.map +1 -0
- package/dist/service/pageValidator.d.ts +25 -0
- package/dist/service/pageValidator.d.ts.map +1 -0
- package/dist/service/pageValidator.js +315 -0
- package/dist/service/pageValidator.js.map +1 -0
- package/dist/service/server.d.ts.map +1 -1
- package/dist/service/server.js +4 -0
- package/dist/service/server.js.map +1 -1
- package/dist/service/sharedTableSchema.d.ts +73 -0
- package/dist/service/sharedTableSchema.d.ts.map +1 -0
- package/dist/service/sharedTableSchema.js +206 -0
- package/dist/service/sharedTableSchema.js.map +1 -0
- package/dist/service/transformPage.d.ts +49 -11
- package/dist/service/transformPage.d.ts.map +1 -1
- package/dist/service/transformPage.js +354 -241
- package/dist/service/transformPage.js.map +1 -1
- package/dist/service/useApiRoutes.d.ts.map +1 -1
- package/dist/service/useApiRoutes.js +288 -34
- package/dist/service/useApiRoutes.js.map +1 -1
- package/dist/service/useConnectorRoutes.d.ts.map +1 -1
- package/dist/service/useConnectorRoutes.js +170 -32
- package/dist/service/useConnectorRoutes.js.map +1 -1
- package/dist/service/useDataRoutes.d.ts.map +1 -1
- package/dist/service/useDataRoutes.js +59 -2
- package/dist/service/useDataRoutes.js.map +1 -1
- package/dist/service/useExtractRoutes.d.ts +4 -0
- package/dist/service/useExtractRoutes.d.ts.map +1 -0
- package/dist/service/useExtractRoutes.js +304 -0
- package/dist/service/useExtractRoutes.js.map +1 -0
- package/dist/service/usePageRoutes.d.ts +17 -0
- package/dist/service/usePageRoutes.d.ts.map +1 -1
- package/dist/service/usePageRoutes.js +1385 -483
- package/dist/service/usePageRoutes.js.map +1 -1
- package/dist/service/useSharedDataRoutes.d.ts.map +1 -1
- package/dist/service/useSharedDataRoutes.js +54 -2
- package/dist/service/useSharedDataRoutes.js.map +1 -1
- package/dist/settings.d.ts +27 -0
- package/dist/settings.d.ts.map +1 -1
- package/dist/settings.js +40 -1
- package/dist/settings.js.map +1 -1
- package/dist/themes.d.ts +0 -5
- package/dist/themes.d.ts.map +1 -1
- package/dist/themes.js +3 -95
- package/dist/themes.js.map +1 -1
- package/migration-rules/v2-to-v3.md +277 -119
- package/package.json +5 -1
- package/{default-pages/application → required-pages/_shell}/page.html +56 -42
- package/required-pages/_shell/page.json +14 -0
- package/required-pages/_starters/page.html +534 -0
- package/required-pages/_starters/page.json +12 -0
- package/required-pages/builder/page.html +353 -43
- package/required-pages/builder/page.json +12 -10
- package/required-pages/pages/page.html +697 -924
- package/required-pages/pages/page.json +12 -10
- package/required-pages/settings/page.html +1879 -1753
- package/required-pages/settings/page.json +12 -10
- package/required-pages/synthos_apis/page.html +834 -845
- package/required-pages/synthos_apis/page.json +12 -10
- package/required-pages/synthos_scripts/page.html +74 -88
- package/required-pages/synthos_scripts/page.json +12 -10
- package/scripts/append-instructions.py +90 -0
- package/scripts/audit-instructions.py +76 -0
- package/scripts/cleanup-shell-markup.mjs +112 -0
- package/service-connectors/buffer/connector.json +46 -0
- package/service-connectors/canva/connector.json +67 -0
- package/service-connectors/elevenlabs/connector.json +1 -1
- package/src/builders/anthropic.ts +150 -25
- package/src/builders/claudecode.ts +310 -0
- package/src/builders/index.ts +7 -1
- package/src/builders/openai.ts +2 -1
- package/src/builders/types.ts +93 -32
- package/src/connectors/types.ts +8 -0
- package/src/init.ts +13 -7
- package/src/migrations.ts +187 -16
- package/src/models/anthropic.ts +140 -30
- package/src/models/chainOfThought.ts +33 -18
- package/src/models/index.ts +2 -2
- package/src/models/providers.ts +10 -1
- package/src/models/types.ts +21 -1
- package/src/pages.ts +271 -35
- package/src/service/createCompletePrompt.ts +6 -0
- package/src/service/mediaCache.ts +206 -0
- package/src/service/pageValidator.ts +337 -0
- package/src/service/server.ts +4 -0
- package/src/service/sharedTableSchema.ts +236 -0
- package/src/service/transformPage.ts +370 -260
- package/src/service/useApiRoutes.ts +282 -32
- package/src/service/useConnectorRoutes.ts +189 -34
- package/src/service/useDataRoutes.ts +198 -116
- package/src/service/useExtractRoutes.ts +331 -0
- package/src/service/usePageRoutes.ts +1411 -394
- package/src/service/useSharedDataRoutes.ts +184 -109
- package/src/settings.ts +65 -0
- package/src/themes.ts +78 -180
- package/starters/blank_starter/chat-history.json +1 -0
- package/starters/blank_starter/page.dark.png +0 -0
- package/starters/blank_starter/page.html +47 -0
- package/starters/blank_starter/page.json +13 -0
- package/starters/blank_starter/page.light.png +0 -0
- package/starters/calculator_starter/chat-history.json +1 -0
- package/starters/calculator_starter/page.dark.png +0 -0
- package/starters/calculator_starter/page.html +232 -0
- package/starters/calculator_starter/page.json +13 -0
- package/starters/calculator_starter/page.light.png +0 -0
- package/starters/calendar_starter/chat-history.json +1 -0
- package/starters/calendar_starter/page.dark.png +0 -0
- package/starters/calendar_starter/page.html +495 -0
- package/starters/calendar_starter/page.json +13 -0
- package/starters/calendar_starter/page.light.png +0 -0
- package/starters/chat_starter/chat-history.json +1 -0
- package/starters/chat_starter/page.dark.png +0 -0
- package/starters/chat_starter/page.html +351 -0
- package/starters/chat_starter/page.json +13 -0
- package/starters/chat_starter/page.light.png +0 -0
- package/starters/checklist_starter/chat-history.json +1 -0
- package/starters/checklist_starter/page.dark.png +0 -0
- package/starters/checklist_starter/page.html +437 -0
- package/starters/checklist_starter/page.json +13 -0
- package/starters/checklist_starter/page.light.png +0 -0
- package/starters/dashboard_starter/chat-history.json +1 -0
- package/starters/dashboard_starter/page.dark.png +0 -0
- package/starters/dashboard_starter/page.html +195 -0
- package/starters/dashboard_starter/page.json +13 -0
- package/starters/dashboard_starter/page.light.png +0 -0
- package/starters/form_starter/chat-history.json +1 -0
- package/starters/form_starter/page.dark.png +0 -0
- package/starters/form_starter/page.html +313 -0
- package/starters/form_starter/page.json +13 -0
- package/starters/form_starter/page.light.png +0 -0
- package/starters/gallery_starter/chat-history.json +1 -0
- package/starters/gallery_starter/page.dark.png +0 -0
- package/starters/gallery_starter/page.html +418 -0
- package/starters/gallery_starter/page.json +13 -0
- package/starters/gallery_starter/page.light.png +0 -0
- package/starters/generator_starter/chat-history.json +1 -0
- package/starters/generator_starter/page.dark.png +0 -0
- package/starters/generator_starter/page.html +261 -0
- package/starters/generator_starter/page.json +13 -0
- package/starters/generator_starter/page.light.png +0 -0
- package/starters/index.html +538 -0
- package/starters/kanban_starter/chat-history.json +1 -0
- package/starters/kanban_starter/page.dark.png +0 -0
- package/starters/kanban_starter/page.html +432 -0
- package/starters/kanban_starter/page.json +13 -0
- package/starters/kanban_starter/page.light.png +0 -0
- package/starters/presentation_builder/chat-history.json +1 -0
- package/starters/presentation_builder/page.dark.png +0 -0
- package/starters/presentation_builder/page.html +970 -0
- package/starters/presentation_builder/page.json +15 -0
- package/starters/presentation_builder/page.light.png +0 -0
- package/starters/presentation_builder/presentation_voice/voice_config.json +9 -0
- package/starters/pulse_starter/chat-history.json +1 -0
- package/starters/pulse_starter/page.dark.png +0 -0
- package/starters/pulse_starter/page.html +698 -0
- package/starters/pulse_starter/page.json +13 -0
- package/starters/pulse_starter/page.light.png +0 -0
- package/starters/quiz_starter/chat-history.json +1 -0
- package/starters/quiz_starter/page.dark.png +0 -0
- package/starters/quiz_starter/page.html +292 -0
- package/starters/quiz_starter/page.json +13 -0
- package/starters/quiz_starter/page.light.png +0 -0
- package/starters/reference_starter/chat-history.json +1 -0
- package/starters/reference_starter/page.dark.png +0 -0
- package/starters/reference_starter/page.html +250 -0
- package/starters/reference_starter/page.json +13 -0
- package/starters/reference_starter/page.light.png +0 -0
- package/starters/retro_game_starter/chat-history.json +1 -0
- package/starters/retro_game_starter/page.dark.png +0 -0
- package/{default-pages → starters}/retro_game_starter/page.html +1281 -1308
- package/starters/retro_game_starter/page.json +15 -0
- package/starters/retro_game_starter/page.light.png +0 -0
- package/starters/roster_starter/chat-history.json +1 -0
- package/starters/roster_starter/page.dark.png +0 -0
- package/starters/roster_starter/page.html +600 -0
- package/starters/roster_starter/page.json +13 -0
- package/starters/roster_starter/page.light.png +0 -0
- package/starters/server.js +182 -0
- package/starters/start.cmd +1 -0
- package/starters/timeline_starter/chat-history.json +1 -0
- package/starters/timeline_starter/page.dark.png +0 -0
- package/starters/timeline_starter/page.html +446 -0
- package/starters/timeline_starter/page.json +13 -0
- package/starters/timeline_starter/page.light.png +0 -0
- package/starters/tutorial_starter/chat-history.json +1 -0
- package/starters/tutorial_starter/page.dark.png +0 -0
- package/starters/tutorial_starter/page.html +283 -0
- package/starters/tutorial_starter/page.json +13 -0
- package/starters/tutorial_starter/page.light.png +0 -0
- package/static-files/agent.v3.js +122 -0
- package/static-files/connector.v3.js +48 -0
- package/static-files/extract.v3.js +188 -0
- package/static-files/helpers.v3.js +50 -6
- package/static-files/page-bridge.js +114 -0
- package/static-files/page.v3.js +1292 -1290
- package/static-files/script.v3.js +32 -0
- package/static-files/server.v3.js +89 -0
- package/static-files/shell-bridge.v3.js +174 -0
- package/static-files/shell-modals.v3.js +521 -0
- package/static-files/{shell.css → shell.v3.css} +271 -22
- package/static-files/shell.v3.js +1865 -0
- package/static-files/storage.v3.js +176 -0
- package/tests/anthropic.spec.ts +42 -7
- package/tests/builders.spec.ts +70 -2
- package/tests/pageValidator.spec.ts +548 -0
- package/tests/profiles.spec.ts +122 -0
- package/tests/sharedTableSchema.spec.ts +242 -0
- package/tests/transformPage.spec.ts +62 -81
- package/default-pages/application/page.json +0 -10
- package/default-pages/retro_game_starter/page.json +0 -12
- package/default-pages/sidebar_page/page.html +0 -51
- package/default-pages/sidebar_page/page.json +0 -10
- package/default-pages/two-panel_page/page.html +0 -68
- package/default-pages/two-panel_page/page.json +0 -10
package/src/pages.ts
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import {checkIfExists, listFolders, loadFile} from './files';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { SynthOSConfig } from './init';
|
|
4
|
+
import { SectionMode } from './builders/types';
|
|
5
|
+
|
|
6
|
+
const VALID_SECTION_MODES: ReadonlySet<SectionMode> = new Set<SectionMode>([
|
|
7
|
+
'always-full',
|
|
8
|
+
'classifier-decides',
|
|
9
|
+
'always-omit-when-empty',
|
|
10
|
+
]);
|
|
4
11
|
|
|
5
12
|
/**
|
|
6
13
|
* Derive the list of required page names by scanning *.html files
|
|
@@ -28,6 +35,7 @@ export const PAGE_VERSION = 3;
|
|
|
28
35
|
export interface PageInfo {
|
|
29
36
|
name: string;
|
|
30
37
|
title: string;
|
|
38
|
+
description: string; // one-line summary of what the page does
|
|
31
39
|
categories: string[];
|
|
32
40
|
pinned: boolean;
|
|
33
41
|
showInAll: boolean; // true = visible in "All" filter, false = only in category filters
|
|
@@ -35,6 +43,14 @@ export interface PageInfo {
|
|
|
35
43
|
lastModified: string; // ISO 8601, empty string if unknown
|
|
36
44
|
pageVersion: number; // integer, 0 = pre-versioning
|
|
37
45
|
mode: 'unlocked' | 'locked';
|
|
46
|
+
greeting: string; // initial greeting shown when page loads
|
|
47
|
+
firstRunGreeting: string; // greeting shown when ?firstRun=true (e.g. builder first launch)
|
|
48
|
+
/**
|
|
49
|
+
* Per-page overrides for builder context-section modes. Maps a section
|
|
50
|
+
* title (e.g. "<FLUENTLM_COMPONENTS>") to a SectionMode that overrides
|
|
51
|
+
* the producer-declared default. Optional and rarely needed.
|
|
52
|
+
*/
|
|
53
|
+
sectionModes?: Record<string, SectionMode>;
|
|
38
54
|
}
|
|
39
55
|
|
|
40
56
|
export type PageMetadata = Omit<PageInfo, 'name'>;
|
|
@@ -75,8 +91,9 @@ export async function loadPageMetadata(config: SynthOSConfig, name: string, fall
|
|
|
75
91
|
}
|
|
76
92
|
|
|
77
93
|
export function parseMetadata(parsed: Record<string, unknown>): PageMetadata {
|
|
78
|
-
|
|
94
|
+
const result: PageMetadata = {
|
|
79
95
|
title: typeof parsed.title === 'string' ? parsed.title : '',
|
|
96
|
+
description: typeof parsed.description === 'string' ? parsed.description : '',
|
|
80
97
|
categories: Array.isArray(parsed.categories) ? parsed.categories : [],
|
|
81
98
|
pinned: typeof parsed.pinned === 'boolean' ? parsed.pinned : false,
|
|
82
99
|
showInAll: typeof parsed.showInAll === 'boolean' ? parsed.showInAll : true,
|
|
@@ -85,7 +102,19 @@ export function parseMetadata(parsed: Record<string, unknown>): PageMetadata {
|
|
|
85
102
|
pageVersion: typeof parsed.pageVersion === 'number' ? parsed.pageVersion
|
|
86
103
|
: typeof parsed.uxVersion === 'number' ? parsed.uxVersion : 0,
|
|
87
104
|
mode: parsed.mode === 'locked' ? 'locked' : 'unlocked',
|
|
105
|
+
greeting: typeof parsed.greeting === 'string' ? parsed.greeting : '',
|
|
106
|
+
firstRunGreeting: typeof parsed.firstRunGreeting === 'string' ? parsed.firstRunGreeting : '',
|
|
88
107
|
};
|
|
108
|
+
if (parsed.sectionModes && typeof parsed.sectionModes === 'object' && !Array.isArray(parsed.sectionModes)) {
|
|
109
|
+
const out: Record<string, SectionMode> = {};
|
|
110
|
+
for (const [title, mode] of Object.entries(parsed.sectionModes as Record<string, unknown>)) {
|
|
111
|
+
if (typeof mode === 'string' && VALID_SECTION_MODES.has(mode as SectionMode)) {
|
|
112
|
+
out[title] = mode as SectionMode;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (Object.keys(out).length > 0) result.sectionModes = out;
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
89
118
|
}
|
|
90
119
|
|
|
91
120
|
export async function savePageMetadata(config: SynthOSConfig, name: string, metadata: PageMetadata): Promise<void> {
|
|
@@ -97,6 +126,7 @@ export async function savePageMetadata(config: SynthOSConfig, name: string, meta
|
|
|
97
126
|
|
|
98
127
|
const DEFAULT_METADATA: PageMetadata = {
|
|
99
128
|
title: '',
|
|
129
|
+
description: '',
|
|
100
130
|
categories: [],
|
|
101
131
|
pinned: false,
|
|
102
132
|
showInAll: true,
|
|
@@ -104,6 +134,8 @@ const DEFAULT_METADATA: PageMetadata = {
|
|
|
104
134
|
lastModified: '',
|
|
105
135
|
pageVersion: 0,
|
|
106
136
|
mode: 'unlocked',
|
|
137
|
+
greeting: '',
|
|
138
|
+
firstRunGreeting: '',
|
|
107
139
|
};
|
|
108
140
|
|
|
109
141
|
export async function listPages(config: SynthOSConfig, fallbackPagesFolders: string[]): Promise<PageInfo[]> {
|
|
@@ -142,6 +174,7 @@ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: str
|
|
|
142
174
|
pageMap.set(name, {
|
|
143
175
|
name,
|
|
144
176
|
title,
|
|
177
|
+
description: '',
|
|
145
178
|
categories,
|
|
146
179
|
pinned: false,
|
|
147
180
|
showInAll: true,
|
|
@@ -149,6 +182,8 @@ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: str
|
|
|
149
182
|
lastModified: '',
|
|
150
183
|
pageVersion: 1,
|
|
151
184
|
mode: 'unlocked',
|
|
185
|
+
greeting: '',
|
|
186
|
+
firstRunGreeting: '',
|
|
152
187
|
});
|
|
153
188
|
}
|
|
154
189
|
}
|
|
@@ -165,6 +200,7 @@ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: str
|
|
|
165
200
|
pageMap.set(name, {
|
|
166
201
|
name,
|
|
167
202
|
title: metadata?.title ?? '',
|
|
203
|
+
description: metadata?.description ?? '',
|
|
168
204
|
categories: metadata?.categories ?? ['System'],
|
|
169
205
|
pinned: metadata?.pinned ?? true,
|
|
170
206
|
showInAll: metadata?.showInAll ?? true,
|
|
@@ -172,6 +208,8 @@ export async function listPages(config: SynthOSConfig, fallbackPagesFolders: str
|
|
|
172
208
|
lastModified: metadata?.lastModified ?? '',
|
|
173
209
|
pageVersion: metadata?.pageVersion ?? 0,
|
|
174
210
|
mode: metadata?.mode ?? 'unlocked',
|
|
211
|
+
greeting: metadata?.greeting ?? '',
|
|
212
|
+
firstRunGreeting: metadata?.firstRunGreeting ?? '',
|
|
175
213
|
});
|
|
176
214
|
}
|
|
177
215
|
}
|
|
@@ -187,11 +225,14 @@ export async function loadPageState(config: SynthOSConfig, name: string): Promis
|
|
|
187
225
|
const pagesFolder = config.pagesFolder;
|
|
188
226
|
const sp = config.storageProvider;
|
|
189
227
|
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
228
|
+
// Migrate any legacy v-scheme files on first read, then look for the
|
|
229
|
+
// highest-numbered page.c<N>.html as the active working state.
|
|
230
|
+
await migrateLegacyChangeFiles(config, name);
|
|
231
|
+
|
|
232
|
+
const latestChange = await getLatestChangeHtmlNumber(config, name);
|
|
233
|
+
if (latestChange > 0) {
|
|
234
|
+
const html = await loadChangeHtml(config, name, latestChange);
|
|
235
|
+
if (html) return html;
|
|
195
236
|
}
|
|
196
237
|
|
|
197
238
|
// Fall back to saved baseline
|
|
@@ -225,6 +266,7 @@ export async function savePageState(config: SynthOSConfig, name: string, content
|
|
|
225
266
|
const now = new Date().toISOString();
|
|
226
267
|
const metadata: PageMetadata = {
|
|
227
268
|
title: title ?? '',
|
|
269
|
+
description: '',
|
|
228
270
|
categories: categories ?? [],
|
|
229
271
|
pinned: false,
|
|
230
272
|
showInAll: true,
|
|
@@ -232,6 +274,8 @@ export async function savePageState(config: SynthOSConfig, name: string, content
|
|
|
232
274
|
lastModified: now,
|
|
233
275
|
pageVersion: PAGE_VERSION,
|
|
234
276
|
mode: 'unlocked',
|
|
277
|
+
greeting: '',
|
|
278
|
+
firstRunGreeting: '',
|
|
235
279
|
};
|
|
236
280
|
await sp.saveFile(metadataPath, JSON.stringify(metadata, null, 4));
|
|
237
281
|
}
|
|
@@ -256,61 +300,250 @@ export async function deletePage(config: SynthOSConfig, name: string): Promise<v
|
|
|
256
300
|
}
|
|
257
301
|
|
|
258
302
|
// ---------------------------------------------------------------------------
|
|
259
|
-
//
|
|
303
|
+
// Per-change journal — durable HTML + JSON pairs per user turn
|
|
260
304
|
// ---------------------------------------------------------------------------
|
|
305
|
+
//
|
|
306
|
+
// Every user message creates a new page.c<N>.json (user msg first, assistant
|
|
307
|
+
// reply or error appended). On a successful transform, page.c<N>.html is also
|
|
308
|
+
// written. Errors only append to the current JSON — no HTML is written — so
|
|
309
|
+
// the HTML and JSON sequence numbers may diverge (gaps allowed).
|
|
310
|
+
//
|
|
311
|
+
// Active page = highest N where page.c<N>.html exists (else baseline).
|
|
312
|
+
// Save collapses the chain back to page.html and deletes all c-files.
|
|
313
|
+
|
|
314
|
+
export interface ChatMessage {
|
|
315
|
+
role: 'user' | 'assistant';
|
|
316
|
+
content: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const CHANGE_HTML_RE = /^page\.c(\d+)\.html$/;
|
|
320
|
+
const CHANGE_JSON_RE = /^page\.c(\d+)\.json$/;
|
|
321
|
+
|
|
322
|
+
function pageFolderPath(config: SynthOSConfig, name: string): string {
|
|
323
|
+
return path.join(config.pagesFolder, 'pages', name);
|
|
324
|
+
}
|
|
261
325
|
|
|
262
326
|
/**
|
|
263
|
-
* Save
|
|
327
|
+
* Save the HTML produced by change <n>: page.c<n>.html
|
|
264
328
|
*/
|
|
265
|
-
export async function
|
|
329
|
+
export async function saveChangeHtml(config: SynthOSConfig, name: string, n: number, html: string): Promise<void> {
|
|
266
330
|
const sp = config.storageProvider;
|
|
267
|
-
const
|
|
268
|
-
await sp.ensureFolderExists(
|
|
269
|
-
await sp.saveFile(path.join(
|
|
331
|
+
const folder = pageFolderPath(config, name);
|
|
332
|
+
await sp.ensureFolderExists(folder);
|
|
333
|
+
await sp.saveFile(path.join(folder, `page.c${n}.html`), html);
|
|
270
334
|
}
|
|
271
335
|
|
|
272
336
|
/**
|
|
273
|
-
* Load
|
|
337
|
+
* Load the HTML produced by change <n> (undefined if missing).
|
|
274
338
|
*/
|
|
275
|
-
export async function
|
|
339
|
+
export async function loadChangeHtml(config: SynthOSConfig, name: string, n: number): Promise<string | undefined> {
|
|
276
340
|
const sp = config.storageProvider;
|
|
277
|
-
const
|
|
278
|
-
if (!await sp.checkIfExists(
|
|
279
|
-
return sp.loadFile(
|
|
341
|
+
const file = path.join(pageFolderPath(config, name), `page.c${n}.html`);
|
|
342
|
+
if (!await sp.checkIfExists(file)) return undefined;
|
|
343
|
+
return sp.loadFile(file);
|
|
280
344
|
}
|
|
281
345
|
|
|
282
346
|
/**
|
|
283
|
-
*
|
|
347
|
+
* Highest N such that page.c<N>.html exists (0 if none).
|
|
284
348
|
*/
|
|
285
|
-
export async function
|
|
349
|
+
export async function getLatestChangeHtmlNumber(config: SynthOSConfig, name: string): Promise<number> {
|
|
350
|
+
const nums = await listChangeNumbers(config, name, CHANGE_HTML_RE);
|
|
351
|
+
return nums.length > 0 ? nums[nums.length - 1] : 0;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Highest N across both .html and .json files (0 if none) — used when
|
|
356
|
+
* assigning the next change number so errors (JSON only) still advance.
|
|
357
|
+
*/
|
|
358
|
+
export async function getLatestChangeNumber(config: SynthOSConfig, name: string): Promise<number> {
|
|
359
|
+
const htmlNums = await listChangeNumbers(config, name, CHANGE_HTML_RE);
|
|
360
|
+
const jsonNums = await listChangeNumbers(config, name, CHANGE_JSON_RE);
|
|
361
|
+
const all = [...htmlNums, ...jsonNums];
|
|
362
|
+
return all.length > 0 ? Math.max(...all) : 0;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* List all change numbers matching a pattern, sorted ascending.
|
|
367
|
+
*/
|
|
368
|
+
async function listChangeNumbers(config: SynthOSConfig, name: string, pattern: RegExp): Promise<number[]> {
|
|
286
369
|
const sp = config.storageProvider;
|
|
287
|
-
const
|
|
288
|
-
if (!await sp.checkIfExists(
|
|
289
|
-
const files = await sp.listFiles(
|
|
290
|
-
|
|
370
|
+
const folder = pageFolderPath(config, name);
|
|
371
|
+
if (!await sp.checkIfExists(folder)) return [];
|
|
372
|
+
const files = await sp.listFiles(folder);
|
|
373
|
+
const nums: number[] = [];
|
|
291
374
|
for (const file of files) {
|
|
292
|
-
const match = file.match(
|
|
293
|
-
if (match)
|
|
294
|
-
|
|
295
|
-
|
|
375
|
+
const match = file.match(pattern);
|
|
376
|
+
if (match) nums.push(parseInt(match[1], 10));
|
|
377
|
+
}
|
|
378
|
+
nums.sort((a, b) => a - b);
|
|
379
|
+
return nums;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Save the messages list for change <n>: page.c<n>.json
|
|
384
|
+
*/
|
|
385
|
+
export async function saveChangeMessages(config: SynthOSConfig, name: string, n: number, messages: ChatMessage[]): Promise<void> {
|
|
386
|
+
const sp = config.storageProvider;
|
|
387
|
+
const folder = pageFolderPath(config, name);
|
|
388
|
+
await sp.ensureFolderExists(folder);
|
|
389
|
+
await sp.saveFile(path.join(folder, `page.c${n}.json`), JSON.stringify({ messages }, null, 2));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Load the messages list for change <n>. Returns [] if the file doesn't
|
|
394
|
+
* exist or is corrupted.
|
|
395
|
+
*/
|
|
396
|
+
export async function loadChangeMessages(config: SynthOSConfig, name: string, n: number): Promise<ChatMessage[]> {
|
|
397
|
+
const sp = config.storageProvider;
|
|
398
|
+
const file = path.join(pageFolderPath(config, name), `page.c${n}.json`);
|
|
399
|
+
if (!await sp.checkIfExists(file)) return [];
|
|
400
|
+
try {
|
|
401
|
+
const raw = await sp.loadFile(file);
|
|
402
|
+
const parsed = JSON.parse(raw);
|
|
403
|
+
if (parsed && Array.isArray(parsed.messages)) return parsed.messages;
|
|
404
|
+
// Legacy shape: top-level array
|
|
405
|
+
if (Array.isArray(parsed)) return parsed;
|
|
406
|
+
} catch {
|
|
407
|
+
// corrupted — fall through
|
|
408
|
+
}
|
|
409
|
+
return [];
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Append a single message to the current page.c<n>.json (creating it if
|
|
414
|
+
* missing). Used for errors — always targets the highest existing N, or
|
|
415
|
+
* creates c1 if none exist.
|
|
416
|
+
*/
|
|
417
|
+
export async function appendMessageToCurrentChange(config: SynthOSConfig, name: string, message: ChatMessage): Promise<number> {
|
|
418
|
+
let n = await getLatestChangeNumber(config, name);
|
|
419
|
+
if (n === 0) n = 1;
|
|
420
|
+
const existing = await loadChangeMessages(config, name, n);
|
|
421
|
+
existing.push(message);
|
|
422
|
+
await saveChangeMessages(config, name, n, existing);
|
|
423
|
+
return n;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Flat concat of all page.c<n>.json messages in numeric order.
|
|
428
|
+
*/
|
|
429
|
+
export async function loadAllChangeMessages(config: SynthOSConfig, name: string): Promise<ChatMessage[]> {
|
|
430
|
+
const nums = await listChangeNumbers(config, name, CHANGE_JSON_RE);
|
|
431
|
+
const out: ChatMessage[] = [];
|
|
432
|
+
for (const n of nums) {
|
|
433
|
+
const msgs = await loadChangeMessages(config, name, n);
|
|
434
|
+
out.push(...msgs);
|
|
435
|
+
}
|
|
436
|
+
return out;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Delete a single change's html + json pair (either may be missing).
|
|
441
|
+
*/
|
|
442
|
+
export async function deleteChange(config: SynthOSConfig, name: string, n: number): Promise<void> {
|
|
443
|
+
const sp = config.storageProvider;
|
|
444
|
+
const folder = pageFolderPath(config, name);
|
|
445
|
+
const html = path.join(folder, `page.c${n}.html`);
|
|
446
|
+
const json = path.join(folder, `page.c${n}.json`);
|
|
447
|
+
if (await sp.checkIfExists(html)) await sp.deleteFile(html);
|
|
448
|
+
if (await sp.checkIfExists(json)) await sp.deleteFile(json);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Delete all page.c*.html and page.c*.json files for a page.
|
|
453
|
+
*/
|
|
454
|
+
export async function clearChanges(config: SynthOSConfig, name: string): Promise<void> {
|
|
455
|
+
const sp = config.storageProvider;
|
|
456
|
+
const folder = pageFolderPath(config, name);
|
|
457
|
+
if (!await sp.checkIfExists(folder)) return;
|
|
458
|
+
const files = await sp.listFiles(folder);
|
|
459
|
+
for (const file of files) {
|
|
460
|
+
if (CHANGE_HTML_RE.test(file) || CHANGE_JSON_RE.test(file)) {
|
|
461
|
+
await sp.deleteFile(path.join(folder, file));
|
|
296
462
|
}
|
|
297
463
|
}
|
|
298
|
-
return max;
|
|
299
464
|
}
|
|
300
465
|
|
|
301
466
|
/**
|
|
302
|
-
*
|
|
467
|
+
* One-shot lazy migration: on first touch of a page, rename any
|
|
468
|
+
* legacy page.v<N>.html to page.c<N>.html and split any legacy
|
|
469
|
+
* chat-history.json into page.c<N>.json files grouped by user turn.
|
|
470
|
+
* Idempotent — does nothing if no legacy files remain.
|
|
303
471
|
*/
|
|
304
|
-
export async function
|
|
472
|
+
export async function migrateLegacyChangeFiles(config: SynthOSConfig, name: string): Promise<void> {
|
|
305
473
|
const sp = config.storageProvider;
|
|
306
|
-
const
|
|
307
|
-
if (!await sp.checkIfExists(
|
|
308
|
-
|
|
474
|
+
const folder = pageFolderPath(config, name);
|
|
475
|
+
if (!await sp.checkIfExists(folder)) return;
|
|
476
|
+
|
|
477
|
+
const files = await sp.listFiles(folder);
|
|
478
|
+
|
|
479
|
+
// 1. Rename page.v<N>.html → page.c<N>.html if target doesn't already exist.
|
|
309
480
|
for (const file of files) {
|
|
310
|
-
|
|
311
|
-
|
|
481
|
+
const match = file.match(/^page\.v(\d+)\.html$/);
|
|
482
|
+
if (!match) continue;
|
|
483
|
+
const n = match[1];
|
|
484
|
+
const src = path.join(folder, file);
|
|
485
|
+
const dst = path.join(folder, `page.c${n}.html`);
|
|
486
|
+
if (await sp.checkIfExists(dst)) {
|
|
487
|
+
// c-file already present — drop the legacy v copy to avoid dual state
|
|
488
|
+
await sp.deleteFile(src);
|
|
489
|
+
continue;
|
|
312
490
|
}
|
|
491
|
+
const html = await sp.loadFile(src);
|
|
492
|
+
await sp.saveFile(dst, html);
|
|
493
|
+
await sp.deleteFile(src);
|
|
313
494
|
}
|
|
495
|
+
|
|
496
|
+
// 2. Split legacy chat-history.json into per-turn page.c<N>.json files,
|
|
497
|
+
// but only if no page.c*.json files already exist (avoid clobbering).
|
|
498
|
+
const chatHistoryPath = path.join(folder, 'chat-history.json');
|
|
499
|
+
if (!await sp.checkIfExists(chatHistoryPath)) return;
|
|
500
|
+
|
|
501
|
+
const existingJson = files.some(f => CHANGE_JSON_RE.test(f));
|
|
502
|
+
if (existingJson) {
|
|
503
|
+
// Already migrated or mid-flight — drop the legacy file to converge.
|
|
504
|
+
await sp.deleteFile(chatHistoryPath);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
let parsed: unknown;
|
|
509
|
+
try {
|
|
510
|
+
parsed = JSON.parse(await sp.loadFile(chatHistoryPath));
|
|
511
|
+
} catch {
|
|
512
|
+
await sp.deleteFile(chatHistoryPath);
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
if (!Array.isArray(parsed)) {
|
|
516
|
+
await sp.deleteFile(chatHistoryPath);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Group messages by user turn. The legacy file starts with a greeting
|
|
521
|
+
// (assistant) followed by user/assistant pairs. Drop the leading greeting
|
|
522
|
+
// (shell rebuilds it from metadata), then chunk on every user message.
|
|
523
|
+
const msgs = parsed as ChatMessage[];
|
|
524
|
+
const groups: ChatMessage[][] = [];
|
|
525
|
+
let current: ChatMessage[] = [];
|
|
526
|
+
let sawFirstUser = false;
|
|
527
|
+
for (const m of msgs) {
|
|
528
|
+
if (!m || typeof m.role !== 'string' || typeof m.content !== 'string') continue;
|
|
529
|
+
if (m.role === 'user') {
|
|
530
|
+
if (current.length > 0) groups.push(current);
|
|
531
|
+
current = [m];
|
|
532
|
+
sawFirstUser = true;
|
|
533
|
+
} else if (sawFirstUser) {
|
|
534
|
+
current.push(m);
|
|
535
|
+
}
|
|
536
|
+
// assistant messages before the first user msg (greeting) are skipped
|
|
537
|
+
}
|
|
538
|
+
if (current.length > 0) groups.push(current);
|
|
539
|
+
|
|
540
|
+
for (let i = 0; i < groups.length; i++) {
|
|
541
|
+
const n = i + 1;
|
|
542
|
+
const target = path.join(folder, `page.c${n}.json`);
|
|
543
|
+
await sp.saveFile(target, JSON.stringify({ messages: groups[i] }, null, 2));
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
await sp.deleteFile(chatHistoryPath);
|
|
314
547
|
}
|
|
315
548
|
|
|
316
549
|
export interface CopyPageOptions {
|
|
@@ -375,6 +608,7 @@ export async function copyPage(
|
|
|
375
608
|
const now = new Date().toISOString();
|
|
376
609
|
const metadata: PageMetadata = {
|
|
377
610
|
title,
|
|
611
|
+
description: '',
|
|
378
612
|
categories,
|
|
379
613
|
pinned: false,
|
|
380
614
|
showInAll: true,
|
|
@@ -382,6 +616,8 @@ export async function copyPage(
|
|
|
382
616
|
lastModified: now,
|
|
383
617
|
pageVersion: PAGE_VERSION,
|
|
384
618
|
mode: 'unlocked',
|
|
619
|
+
greeting: '',
|
|
620
|
+
firstRunGreeting: '',
|
|
385
621
|
};
|
|
386
622
|
await savePageMetadata(config, targetName, metadata);
|
|
387
623
|
|
|
@@ -8,6 +8,12 @@ export async function createCompletePrompt(config: SynthOSConfig, use: 'builder'
|
|
|
8
8
|
const settings = await loadSettings(config);
|
|
9
9
|
const entry = getModelEntry(settings, use);
|
|
10
10
|
|
|
11
|
+
// ClaudeCode provider uses its own CLI subprocess — no API key or completePrompt needed.
|
|
12
|
+
// Return a no-op stub; the ClaudeCode builder ignores it entirely.
|
|
13
|
+
if (entry.provider === 'ClaudeCode') {
|
|
14
|
+
return async () => ({ completed: false, error: new Error('ClaudeCode provider does not use completePrompt') });
|
|
15
|
+
}
|
|
16
|
+
|
|
11
17
|
if (!entry.configuration.apiKey) {
|
|
12
18
|
throw new Error('API key not configured');
|
|
13
19
|
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { StorageProvider } from '../storage/StorageProvider';
|
|
4
|
+
|
|
5
|
+
export interface MediaCacheOptions {
|
|
6
|
+
storage: StorageProvider;
|
|
7
|
+
cacheRoot: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CacheEntry {
|
|
11
|
+
hit: true;
|
|
12
|
+
buffer: Buffer;
|
|
13
|
+
contentType: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CacheMiss {
|
|
17
|
+
hit: false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type CacheLookup = CacheEntry | CacheMiss;
|
|
21
|
+
|
|
22
|
+
export interface CacheStats {
|
|
23
|
+
images: { count: number; totalBytes: number };
|
|
24
|
+
audio: { count: number; totalBytes: number };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hashKey(key: string): string {
|
|
28
|
+
return createHash('sha256').update(key).digest('hex');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const MAX_ENTRIES = 500;
|
|
32
|
+
const CATEGORIES = ['images', 'audio'] as const;
|
|
33
|
+
|
|
34
|
+
export function createMediaCache(options: MediaCacheOptions) {
|
|
35
|
+
const { storage, cacheRoot } = options;
|
|
36
|
+
|
|
37
|
+
function categoryDir(category: string): string {
|
|
38
|
+
return path.join(cacheRoot, category);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function ensureCategory(category: string): Promise<void> {
|
|
42
|
+
await storage.ensureFolderExists(categoryDir(category));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function get(category: string, key: string): Promise<CacheLookup> {
|
|
46
|
+
const hash = hashKey(key);
|
|
47
|
+
const dir = categoryDir(category);
|
|
48
|
+
const metaPath = path.join(dir, `${hash}.meta.json`);
|
|
49
|
+
|
|
50
|
+
if (!(await storage.checkIfExists(metaPath))) {
|
|
51
|
+
return { hit: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const meta = JSON.parse(await storage.loadFile(metaPath));
|
|
55
|
+
const dataPath = path.join(dir, `${hash}${meta.extension}`);
|
|
56
|
+
try {
|
|
57
|
+
const buffer = await storage.loadBuffer(dataPath);
|
|
58
|
+
|
|
59
|
+
// Update last access time for eviction tracking
|
|
60
|
+
meta.lastAccessedAt = new Date().toISOString();
|
|
61
|
+
await storage.saveFile(metaPath, JSON.stringify(meta, null, 2));
|
|
62
|
+
|
|
63
|
+
return { hit: true, buffer, contentType: meta.contentType };
|
|
64
|
+
} catch {
|
|
65
|
+
return { hit: false };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function put(
|
|
70
|
+
category: string,
|
|
71
|
+
key: string,
|
|
72
|
+
data: Buffer,
|
|
73
|
+
contentType: string,
|
|
74
|
+
meta: Record<string, unknown>
|
|
75
|
+
): Promise<void> {
|
|
76
|
+
await ensureCategory(category);
|
|
77
|
+
const hash = hashKey(key);
|
|
78
|
+
const dir = categoryDir(category);
|
|
79
|
+
const extension = extensionForContentType(contentType);
|
|
80
|
+
const dataPath = path.join(dir, `${hash}${extension}`);
|
|
81
|
+
const metaPath = path.join(dir, `${hash}.meta.json`);
|
|
82
|
+
|
|
83
|
+
const now = new Date().toISOString();
|
|
84
|
+
await storage.saveBuffer(dataPath, data);
|
|
85
|
+
await storage.saveFile(metaPath, JSON.stringify({
|
|
86
|
+
...meta,
|
|
87
|
+
contentType,
|
|
88
|
+
extension,
|
|
89
|
+
createdAt: now,
|
|
90
|
+
lastAccessedAt: now,
|
|
91
|
+
}, null, 2));
|
|
92
|
+
|
|
93
|
+
// Run eviction after writing the new entry
|
|
94
|
+
await evict();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function evict(): Promise<void> {
|
|
98
|
+
// Collect all entries across categories
|
|
99
|
+
const entries: { category: string; hash: string; lastAccessedAt: string; size: number; metaPath: string; extension: string }[] = [];
|
|
100
|
+
|
|
101
|
+
for (const category of CATEGORIES) {
|
|
102
|
+
const dir = categoryDir(category);
|
|
103
|
+
if (!(await storage.checkIfExists(dir))) continue;
|
|
104
|
+
|
|
105
|
+
const files = await storage.listFiles(dir);
|
|
106
|
+
for (const file of files) {
|
|
107
|
+
if (!file.endsWith('.meta.json')) continue;
|
|
108
|
+
const metaPath = path.join(dir, file);
|
|
109
|
+
try {
|
|
110
|
+
const meta = JSON.parse(await storage.loadFile(metaPath));
|
|
111
|
+
const hash = file.replace('.meta.json', '');
|
|
112
|
+
const dataPath = path.join(dir, `${hash}${meta.extension}`);
|
|
113
|
+
let size = 0;
|
|
114
|
+
try {
|
|
115
|
+
const info = await storage.stat(dataPath);
|
|
116
|
+
size = info.size;
|
|
117
|
+
} catch { /* data file missing, size stays 0 */ }
|
|
118
|
+
entries.push({
|
|
119
|
+
category,
|
|
120
|
+
hash,
|
|
121
|
+
lastAccessedAt: meta.lastAccessedAt || meta.createdAt || '1970-01-01T00:00:00.000Z',
|
|
122
|
+
size,
|
|
123
|
+
metaPath,
|
|
124
|
+
extension: meta.extension,
|
|
125
|
+
});
|
|
126
|
+
} catch { /* skip unreadable meta */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (entries.length <= MAX_ENTRIES) return;
|
|
131
|
+
|
|
132
|
+
// Sort by lastAccessedAt descending — most recent first
|
|
133
|
+
entries.sort((a, b) => b.lastAccessedAt.localeCompare(a.lastAccessedAt));
|
|
134
|
+
|
|
135
|
+
// Evict everything beyond the first 500
|
|
136
|
+
const toEvict = entries.slice(MAX_ENTRIES);
|
|
137
|
+
for (const entry of toEvict) {
|
|
138
|
+
const dir = categoryDir(entry.category);
|
|
139
|
+
const dataPath = path.join(dir, `${entry.hash}${entry.extension}`);
|
|
140
|
+
try { await storage.deleteFile(dataPath); } catch { /* ignore */ }
|
|
141
|
+
try { await storage.deleteFile(entry.metaPath); } catch { /* ignore */ }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function invalidate(category: string, key: string): Promise<void> {
|
|
146
|
+
const hash = hashKey(key);
|
|
147
|
+
const dir = categoryDir(category);
|
|
148
|
+
const metaPath = path.join(dir, `${hash}.meta.json`);
|
|
149
|
+
|
|
150
|
+
if (await storage.checkIfExists(metaPath)) {
|
|
151
|
+
const meta = JSON.parse(await storage.loadFile(metaPath));
|
|
152
|
+
const dataPath = path.join(dir, `${hash}${meta.extension}`);
|
|
153
|
+
try { await storage.deleteFile(dataPath); } catch { /* ignore */ }
|
|
154
|
+
try { await storage.deleteFile(metaPath); } catch { /* ignore */ }
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function clearCategory(category: string): Promise<void> {
|
|
159
|
+
const dir = categoryDir(category);
|
|
160
|
+
if (await storage.checkIfExists(dir)) {
|
|
161
|
+
await storage.deleteFolder(dir);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function clearAll(): Promise<void> {
|
|
166
|
+
await clearCategory('images');
|
|
167
|
+
await clearCategory('audio');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function stats(): Promise<CacheStats> {
|
|
171
|
+
const result: CacheStats = {
|
|
172
|
+
images: { count: 0, totalBytes: 0 },
|
|
173
|
+
audio: { count: 0, totalBytes: 0 },
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
for (const category of CATEGORIES) {
|
|
177
|
+
const dir = categoryDir(category);
|
|
178
|
+
if (!(await storage.checkIfExists(dir))) continue;
|
|
179
|
+
const files = await storage.listFiles(dir);
|
|
180
|
+
for (const file of files) {
|
|
181
|
+
if (file.endsWith('.meta.json')) continue;
|
|
182
|
+
try {
|
|
183
|
+
const info = await storage.stat(path.join(dir, file));
|
|
184
|
+
result[category].count++;
|
|
185
|
+
result[category].totalBytes += info.size;
|
|
186
|
+
} catch { /* skip missing */ }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { get, put, invalidate, clearCategory, clearAll, stats, evict };
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function extensionForContentType(contentType: string): string {
|
|
197
|
+
if (contentType.includes('png')) return '.png';
|
|
198
|
+
if (contentType.includes('jpeg') || contentType.includes('jpg')) return '.jpg';
|
|
199
|
+
if (contentType.includes('webp')) return '.webp';
|
|
200
|
+
if (contentType.includes('mpeg') || contentType.includes('mp3')) return '.mp3';
|
|
201
|
+
if (contentType.includes('wav')) return '.wav';
|
|
202
|
+
if (contentType.includes('ogg')) return '.ogg';
|
|
203
|
+
return '.bin';
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export type MediaCache = ReturnType<typeof createMediaCache>;
|