@@ -150,9 +140,9 @@ const UpcomingTasksWidget = memo(function UpcomingTasksWidget() {
))}
{/* Scheduled Tasks */}
- {scheduledTasks.slice(0, readyTasks.length > 0 ? 2 : 4).map((task) => (
+ {scheduledTasks.slice(0, readyTasks.length > 0 ? 2 : 4).map((task, index) => (
diff --git a/client/src/hooks/useAutoRefetch.js b/client/src/hooks/useAutoRefetch.js
new file mode 100644
index 00000000..5d7d0913
--- /dev/null
+++ b/client/src/hooks/useAutoRefetch.js
@@ -0,0 +1,45 @@
+import { useState, useEffect, useRef } from 'react';
+
+/**
+ * Hook for auto-refetching data on an interval.
+ * Eliminates the repeated useEffect + setInterval pattern across dashboard widgets.
+ *
+ * @param {Function} fetchFn - Async function that returns data (should handle its own errors)
+ * @param {number} intervalMs - Refetch interval in milliseconds (changing this will restart the interval with the new value)
+ * @returns {{ data: any, loading: boolean }}
+ */
+export function useAutoRefetch(fetchFn, intervalMs) {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const fetchRef = useRef(fetchFn);
+
+ // Keep ref current so interval callbacks don't capture stale closures
+ useEffect(() => {
+ fetchRef.current = fetchFn;
+ }, [fetchFn]);
+
+ useEffect(() => {
+ let cancelled = false;
+
+ const loadData = async () => {
+ try {
+ const result = await fetchRef.current();
+ if (cancelled) return;
+ setData(result);
+ setLoading(false);
+ } catch {
+ // Keep prior data on failure, just clear loading state
+ if (!cancelled) setLoading(false);
+ }
+ };
+
+ loadData();
+ const interval = setInterval(loadData, intervalMs);
+ return () => {
+ cancelled = true;
+ clearInterval(interval);
+ };
+ }, [intervalMs]);
+
+ return { data, loading };
+}
diff --git a/client/src/utils/formatters.js b/client/src/utils/formatters.js
index a5fc80f7..86a24d88 100644
--- a/client/src/utils/formatters.js
+++ b/client/src/utils/formatters.js
@@ -9,7 +9,9 @@
* @returns {string} Formatted relative time (e.g., "Just now", "5m ago", "2h ago")
*/
export function formatTime(timestamp) {
+ if (timestamp == null || timestamp === '') return 'Unknown';
const date = new Date(timestamp);
+ if (isNaN(date.getTime())) return 'Invalid date';
const now = new Date();
const diff = now - date;
diff --git a/client/vite.config.js b/client/vite.config.js
index 5e8699e0..02c221ff 100644
--- a/client/vite.config.js
+++ b/client/vite.config.js
@@ -1,7 +1,12 @@
-import { defineConfig } from 'vite';
+import { defineConfig, loadEnv } from 'vite';
import react from '@vitejs/plugin-react';
-export default defineConfig({
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, process.cwd(), 'VITE_');
+ const API_HOST = env.VITE_API_HOST || 'localhost';
+ const API_TARGET = `http://${API_HOST}:5554`;
+
+ return {
plugins: [react()],
server: {
host: '0.0.0.0',
@@ -9,11 +14,11 @@ export default defineConfig({
open: false,
proxy: {
'/api': {
- target: 'http://localhost:5554',
+ target: API_TARGET,
changeOrigin: true
},
'/socket.io': {
- target: 'http://localhost:5554',
+ target: API_TARGET,
changeOrigin: true,
ws: true
}
@@ -39,4 +44,5 @@ export default defineConfig({
// Increase chunk size warning limit (icons are large)
chunkSizeWarningLimit: 600
}
+};
});
diff --git a/docs/features/agent-skills.md b/docs/features/agent-skills.md
new file mode 100644
index 00000000..62770f8a
--- /dev/null
+++ b/docs/features/agent-skills.md
@@ -0,0 +1,42 @@
+# Agent Skill System (M40)
+
+Improves CoS agent accuracy and reliability through task-type-specific prompt templates, context compaction, negative example routing, and deterministic workflow skills. Inspired by [OpenAI Skills & Shell Tips](https://developers.openai.com/blog/skills-shell-tips).
+
+## P1: Task-Type-Specific Agent Prompts (Skill Templates)
+
+Created specialized prompt templates per task category with routing, examples, and guidelines:
+- **Routing descriptions**: "Use when..." / "Don't use when..." sections in each skill template
+- **Embedded examples**: Worked examples of successful completions for each task type
+- **Task-specific guidelines**: Security audit includes OWASP checklist; feature includes validation/convention requirements; refactor emphasizes behavior preservation
+
+**Implementation:**
+- Added `data/prompts/skills/` directory with 6 task-type templates: `bug-fix.md`, `feature.md`, `security-audit.md`, `refactor.md`, `documentation.md`, `mobile-responsive.md`
+- Added `detectSkillTemplate()` and `loadSkillTemplate()` in `subAgentSpawner.js` with keyword-based matching (ordered by specificity -- security/mobile before generic bug-fix/feature)
+- Updated `buildAgentPrompt()` to inject matched skill template into both the Mustache template system and the fallback template
+- Updated `cos-agent-briefing.md` with `{{#skillSection}}` conditional block
+- Templates only loaded when matched to avoid token inflation
+
+## P2: Agent Context Compaction
+
+Long-running agents can hit context limits causing failures. Added proactive context management:
+- Pass `--max-turns` or equivalent context budget hints when spawning agents
+- Track agent output length and detect when agents are approaching context limits
+- Added compaction metadata to agent error analysis so retries can include "compact context" instructions
+- Updated the agent briefing to include explicit output format constraints for verbose task types
+
+## P3: Negative Example Coverage for Task Routing
+
+Improved task-to-model routing accuracy by adding negative examples to the model selection logic:
+- Documented which task types should NOT use light models
+- Added "anti-patterns" to task learning: when a task type fails with a specific model, record the negative signal via `routingAccuracy` cross-reference (taskType x modelTier)
+- Surfaced routing accuracy metrics in the Learning tab so the user can see misroutes
+- Enhanced `suggestModelTier()` to use negative signal data for smarter tier avoidance
+
+## P4: Deterministic Workflow Skills
+
+For recurring autonomous jobs (daily briefing, git maintenance, security audit, app improvement), encoded the full workflow as a deterministic skill:
+- Each skill defines exact steps, expected outputs, and success criteria in `data/prompts/skills/jobs/`
+- Prevents prompt drift across runs -- jobs now load structured skill templates instead of inline prompt strings
+- Skills are versioned and editable via the Prompt Manager UI (Job Skills tab)
+- `generateTaskFromJob()` builds effective prompts from skill template sections (Steps, Expected Outputs, Success Criteria)
+- API routes added: GET/PUT `/api/prompts/skills/jobs/:name`, preview via GET `/api/prompts/skills/jobs/:name/preview`
diff --git a/docs/features/identity-system.md b/docs/features/identity-system.md
new file mode 100644
index 00000000..d4d61529
--- /dev/null
+++ b/docs/features/identity-system.md
@@ -0,0 +1,612 @@
+# Unified Digital Twin Identity System (M42)
+
+Connects Genome (117 markers, 32 categories), Chronotype (5 sleep markers + behavioral), Aesthetic Taste (P2 complete, P2.5 adds twin-aware prompting), and Mortality-Aware Goals into a single coherent Identity architecture with cross-insights engine.
+
+## Motivation
+
+Four separate workstreams converge on the same vision: a personal digital twin that knows *who you are* biologically, temporally, aesthetically, and existentially. Today these live as disconnected features:
+
+| Subsystem | Current State | Location |
+|-----------|--------------|----------|
+| **Genome** | Fully implemented: 23andMe upload, 117 curated SNP markers across 32 categories, ClinVar integration, epigenetic tracking | `server/services/genome.js`, `GenomeTab.jsx`, `data/digital-twin/genome.json` |
+| **Chronotype** | Genetic data ready: 5 sleep/circadian markers (CLOCK rs1801260, DEC2 rs57875989, PER2 rs35333999, CRY1 rs2287161, MTNR1B rs10830963) + `daily_routines` enrichment category. Derivation service not yet built | `curatedGenomeMarkers.js` sleep category, `ENRICHMENT_CATEGORIES.daily_routines` |
+| **Aesthetic Taste** | P2 complete: Taste questionnaire with 5 sections (movies, music, visual_art, architecture, food), conversational Q&A, AI summary generation. Enrichment categories also feed taste data from book/movie/music lists | `TasteTab.jsx`, `taste-questionnaire.js`, `data/digital-twin/taste-profile.json` |
+| **Goal Tracking** | Partially exists: `COS-GOALS.md` for CoS missions, `TASKS.md` for user tasks, `EXISTENTIAL.md` soul doc | `data/COS-GOALS.md`, `data/TASKS.md`, `data/digital-twin/EXISTENTIAL.md` |
+
+These should be unified under a single **Identity** architecture so the twin can reason across all dimensions (e.g., "your CLOCK gene says evening chronotype β schedule deep work after 8pm" or "given your longevity markers and age, here's how to prioritize your 10-year goals").
+
+## Data Model
+
+### Entity: `identity.json` (top-level twin orchestration)
+
+```json
+{
+ "version": "1.0.0",
+ "createdAt": "2026-02-12T00:00:00.000Z",
+ "updatedAt": "2026-02-12T00:00:00.000Z",
+ "sections": {
+ "genome": { "status": "active", "dataFile": "genome.json", "markerCount": 117, "categoryCount": 32, "lastScanAt": "..." },
+ "chronotype": { "status": "active", "dataFile": "chronotype.json", "derivedFrom": ["genome:sleep", "enrichment:daily_routines"] },
+ "aesthetics": { "status": "active", "dataFile": "aesthetics.json", "derivedFrom": ["enrichment:aesthetics", "enrichment:favorite_books", "enrichment:favorite_movies", "enrichment:music_taste"] },
+ "goals": { "status": "active", "dataFile": "goals.json" }
+ },
+ "crossLinks": []
+}
+```
+
+### Entity: Chronotype Profile (`chronotype.json`)
+
+Derived from genome sleep markers + daily_routines enrichment answers + user overrides.
+
+```json
+{
+ "chronotype": "evening",
+ "confidence": 0.75,
+ "sources": {
+ "genetic": {
+ "clockGene": { "rsid": "rs1801260", "genotype": "T/C", "signal": "mild_evening" },
+ "dec2": { "rsid": "rs57875989", "genotype": "G/G", "signal": "standard_sleep_need" },
+ "per2": { "rsid": "rs35333999", "genotype": "C/C", "signal": "standard_circadian" },
+ "cry1": { "rsid": "rs2287161", "genotype": "C/C", "signal": "standard_period" },
+ "mtnr1b": { "rsid": "rs10830963", "genotype": "T/T", "signal": "normal_melatonin_receptor" }
+ },
+ "behavioral": {
+ "preferredWakeTime": "08:30",
+ "preferredSleepTime": "00:30",
+ "peakFocusWindow": "20:00-02:00",
+ "energyDipWindow": "14:00-16:00"
+ }
+ },
+ "recommendations": {
+ "deepWork": "20:00-02:00",
+ "lightTasks": "09:00-12:00",
+ "exercise": "17:00-19:00",
+ "caffeineCutoff": "14:00"
+ },
+ "updatedAt": "2026-02-12T00:00:00.000Z"
+}
+```
+
+**Derivation logic**: Five genome sleep markers provide the genetic baseline: CLOCK (evening preference), DEC2 (sleep duration need), PER2 (circadian period), CRY1 (delayed sleep phase), MTNR1B (melatonin receptor / nighttime glucose). The `daily_routines` enrichment answers provide behavioral confirmation. When genetic and behavioral signals agree, confidence is high. When they disagree, surface the conflict for user review. Caffeine cutoff cross-references caffeine metabolism markers (CYP1A2 rs762551, ADA rs73598374). MTNR1B status also informs late-eating recommendations.
+
+### Entity: Aesthetic Taste Profile (`aesthetics.json`)
+
+Consolidates scattered aesthetic data into a structured profile.
+
+```json
+{
+ "profile": {
+ "visualStyle": [],
+ "narrativePreferences": [],
+ "musicProfile": [],
+ "designPrinciples": [],
+ "antiPatterns": []
+ },
+ "sources": {
+ "enrichmentAnswers": { "aesthetics": "...", "questionsAnswered": 0 },
+ "bookAnalysis": { "themes": [], "sourceDoc": "BOOKS.md" },
+ "movieAnalysis": { "themes": [], "sourceDoc": "MOVIES.md" },
+ "musicAnalysis": { "themes": [], "sourceDoc": "AUDIO.md" }
+ },
+ "questionnaire": {
+ "completed": false,
+ "sections": [
+ "visual_design",
+ "color_and_mood",
+ "architecture_and_space",
+ "fashion_and_texture",
+ "sound_and_music",
+ "narrative_and_story",
+ "anti_preferences"
+ ]
+ },
+ "updatedAt": null
+}
+```
+
+**Derivation logic**: Taste is partially observable from existing enrichment data (book/movie/music lists). The aesthetic questionnaire fills in the rest via prompted sections β each section shows image/description pairs and asks for preference rankings. LLM analysis of existing media lists extracts themes (e.g., "brutalist minimalism", "high-contrast neon", "atmospheric dread") to seed the profile.
+
+### Entity: Mortality-Aware Goals (`goals.json`)
+
+```json
+{
+ "birthDate": "1980-01-15",
+ "lifeExpectancyEstimate": {
+ "baseline": 78.5,
+ "adjusted": null,
+ "adjustmentFactors": {
+ "geneticLongevity": null,
+ "cardiovascularRisk": null,
+ "lifestyle": null
+ },
+ "source": "SSA actuarial table + genome markers"
+ },
+ "timeHorizons": {
+ "yearsRemaining": null,
+ "healthyYearsRemaining": null,
+ "percentLifeComplete": null
+ },
+ "goals": [
+ {
+ "id": "uuid",
+ "title": "...",
+ "description": "...",
+ "horizon": "5-year",
+ "category": "creative|family|health|financial|legacy|mastery",
+ "urgency": null,
+ "status": "active|completed|abandoned",
+ "milestones": [],
+ "createdAt": "...",
+ "updatedAt": "..."
+ }
+ ],
+ "updatedAt": null
+}
+```
+
+**Derivation logic**: Birth date + actuarial baseline + genome longevity/cardiovascular markers produce an adjusted life expectancy. This creates urgency scoring: a "legacy" goal with a 20-year timeline hits differently at 30% life-complete vs 70%. Goals are categorized and scored by time-decay urgency. The system can suggest reprioritization when markers indicate risk factors (e.g., high cardiovascular genetic risk -> prioritize health goals).
+
+## Entity Relationships
+
+```
+ +------------------+
+ | identity.json |
+ | (orchestrator) |
+ +--+---+---+---+--+
+ | | | |
+ +----------+ | | +----------+
+ v v v v
+ +---------+ +----------+ +----------+ +---------+
+ | Genome | |Chronotype| |Aesthetics| | Goals |
+ |genome.json| |chrono.json| |aesth.json| |goals.json|
+ +----+----+ +----+-----+ +----+-----+ +----+----+
+ | | | |
+ | +---------+ | |
+ | | derives from | |
+ +----+ sleep markers | |
+ | | | |
+ | | caffeine cutoff <-----+ |
+ | | from caffeine markers | |
+ | | | |
+ | +-----------------------+ |
+ | | |
+ | longevity/cardio ---------------------->|
+ | markers inform | urgency |
+ | life expectancy | scoring |
+ | | |
+ | +-------------+ |
+ | | derives from |
+ | | enrichment: aesthetics, |
+ | | books, movies, music |
+ | | |
+ +--------------+----------------------------+
+ All reference meta.json
+ (documents, enrichment, traits)
+```
+
+**Cross-cutting links** (stored in `identity.json.crossLinks`):
+- `genome:sleep` -> `chronotype:genetic` (CLOCK/DEC2/PER2/CRY1/MTNR1B markers feed chronotype)
+- `genome:caffeine` -> `chronotype:recommendations.caffeineCutoff` (CYP1A2/ADA markers set cutoff)
+- `genome:sleep:mtnr1b` -> `chronotype:recommendations.lateEatingCutoff` (MTNR1B impairs nighttime glucose)
+- `genome:longevity` + `genome:cardiovascular` -> `goals:lifeExpectancyEstimate` (risk-adjusted lifespan)
+- `enrichment:daily_routines` -> `chronotype:behavioral` (self-reported schedule)
+- `enrichment:aesthetics` + `enrichment:favorite_*` + `enrichment:music_taste` -> `aesthetics:profile` (taste extraction)
+- `traits:valuesHierarchy` -> `goals:category` priority weighting (autonomy-valuing person weights mastery goals higher)
+
+## Identity Page Structure
+
+The existing Digital Twin page at `/digital-twin/:tab` gets a new **Identity** tab that serves as the unified view. Individual subsystem tabs (Genome, Enrich) remain for deep dives.
+
+### Route: `/digital-twin/identity`
+
+```
++-------------------------------------------------------------+
+| Digital Twin |
+| Overview | Documents | ... | Identity | Genome | ... |
++-------------------------------------------------------------+
+| |
+| +- Identity Dashboard --------------------------------+ |
+| | Completeness: xxxxxxxx.. 72% | |
+| | 4 sections: Genome Y Chronotype ~ Taste . Goals .| |
+| +-----------------------------------------------------+ |
+| |
+| +- Genome Summary Card -------------------------------+ |
+| | 117 markers scanned across 32 categories | |
+| | Key findings: ~20 beneficial, ~40 concern, ~5 major| |
+| | [View Full Genome ->] | |
+| +-----------------------------------------------------+ |
+| |
+| +- Chronotype Card -----------------------------------+ |
+| | Type: Evening Owl (75% confidence from 5 markers) | |
+| | Genetic: CLOCK T/C + CRY1 C/C + PER2 C/C + DEC2 G| |
+| | Peak focus: 8pm-2am | Caffeine cutoff: 2pm | |
+| | Late eating cutoff: 8pm (MTNR1B-informed) | |
+| | [Configure Schedule ->] | |
+| +-----------------------------------------------------+ |
+| |
+| +- Aesthetic Taste Card -------------------------------+ |
+| | Taste Tab: 0/5 sections completed (P2 UI ready) | |
+| | Detected themes from media: brutalist, atmospheric | |
+| | [Continue Taste Questionnaire ->] [Go to Taste ->] | |
+| +-----------------------------------------------------+ |
+| |
+| +- Life Goals Card -----------------------------------+ |
+| | Status: Not configured | |
+| | Set birth date and goals to enable mortality-aware | |
+| | priority scoring | |
+| | [Set Up Goals ->] | |
+| +-----------------------------------------------------+ |
+| |
+| +- Cross-Insights ------------------------------------+ |
+| | "Your CLOCK gene evening tendency + caffeine | |
+| | sensitivity suggest cutting coffee by 2pm" | |
+| | "Longevity marker FOXO3A T/T (concern) + IL-6 C/C | |
+| | (inflammation concern) -- prioritize health goals" | |
+| +-----------------------------------------------------+ |
+| |
++-------------------------------------------------------------+
+```
+
+### Sub-routes for deep dives:
+- `/digital-twin/identity` -- Dashboard overview (above)
+- `/digital-twin/identity/chronotype` -- Full chronotype editor with schedule builder
+- `/digital-twin/identity/taste` -- Aesthetic questionnaire flow (section-by-section)
+- `/digital-twin/identity/goals` -- Goal CRUD with urgency visualization
+- `/digital-twin/genome` -- Existing genome tab (unchanged)
+
+## Implementation Phases
+
+### P1: Identity Orchestrator & Chronotype (data layer)
+- Create `data/digital-twin/identity.json` with section status tracking
+- Create `server/services/identity.js` -- orchestrator that reads from genome, enrichment, taste-profile, and new data files
+- Create `data/digital-twin/chronotype.json` -- derive from 5 genome sleep markers + daily_routines enrichment
+- Add `GET /api/digital-twin/identity` route returning unified section status
+- Add `GET/PUT /api/digital-twin/identity/chronotype` routes
+- Derivation function: `deriveChronotypeFromGenome(genomeSummary)` extracts all 5 sleep markers (CLOCK, DEC2, PER2, CRY1, MTNR1B) -> composite chronotype signal with weighted confidence
+- Cross-reference CYP1A2/ADA caffeine markers and MTNR1B melatonin receptor for caffeine cutoff and late-eating recommendations
+
+### P2: Aesthetic Taste Questionnaire (complete)
+- Created `data/digital-twin/taste-profile.json` for structured taste preference storage
+- Created `server/services/taste-questionnaire.js` with 5 taste sections (movies, music, visual_art, architecture, food), each with core questions and branching follow-ups triggered by keyword detection
+- Added 7 API routes under `/api/digital-twin/taste/*` (profile, sections, next question, answer, responses, summary, reset)
+- Built `TasteTab.jsx` conversational Q&A UI with section grid, question flow, review mode, and AI-powered summary generation
+- Responses persisted to taste-profile.json and appended to AESTHETICS.md for digital twin context
+- Added Taste tab to Digital Twin page navigation
+
+### P2.5: Digital Twin Aesthetic Taste Prompting (brain idea 608dc733)
+
+#### Problem
+
+P2's Taste questionnaire uses static questions and keyword-triggered follow-ups. The questions are good but generic -- they don't reference anything the twin already knows about the user. Brain idea 608dc733 proposes using the digital twin's existing knowledge (books, music, movie lists, enrichment answers, personality traits) to generate personalized, conversational prompts that feel like talking to someone who already knows you rather than filling out a survey.
+
+#### What Data to Capture
+
+The aesthetic taste system captures preferences across **7 domains**, extending P2's 5 sections with 2 new ones (fashion/texture and digital/interface):
+
+| Domain | Data Captured | Sources That Seed It |
+|--------|--------------|---------------------|
+| **Movies & Film** | Visual style preferences, narrative structure, mood/atmosphere, genre affinities, anti-preferences, formative films | BOOKS.md (narrative taste), enrichment:favorite_movies, existing P2 responses |
+| **Music & Sound** | Functional use (focus/energy/decompress), genre affinities, production preferences, anti-sounds, formative artists | AUDIO.md, enrichment:music_taste, existing P2 responses |
+| **Visual Art & Design** | Minimalism vs maximalism spectrum, color palette preferences, design movements, typography, layout sensibility | CREATIVE.md, enrichment:aesthetics, existing P2 responses |
+| **Architecture & Spaces** | Material preferences, light quality, scale/intimacy, indoor-outdoor relationship, sacred vs functional | enrichment:aesthetics, existing P2 responses |
+| **Food & Culinary** | Flavor profiles, cuisine affinities, cooking philosophy, dining experience priorities, sensory texture preferences | enrichment:daily_routines (meal patterns), existing P2 responses |
+| **Fashion & Texture** *(new)* | Material/fabric preferences, silhouette comfort, color wardrobe, formality spectrum, tactile sensitivity | genome:sensory markers (if available), enrichment:aesthetics |
+| **Digital & Interface** *(new)* | Dark vs light mode, information density, animation tolerance, typography preferences, notification style, tool aesthetics | PREFERENCES.md, existing PortOS theme choices (port-bg, port-card etc.) |
+
+Each domain captures:
+- **Positive affinities** -- what they're drawn to and why
+- **Anti-preferences** -- what they actively avoid (often more revealing than likes)
+- **Functional context** -- how the preference serves them (focus, comfort, identity, social)
+- **Formative influences** -- early experiences that shaped the preference
+- **Evolution** -- how the preference has changed over time
+
+#### Conversational Prompting Flow
+
+The key design principle: **conversation, not survey**. The twin generates questions that reference things it already knows, creating a dialogue that feels like it's building on shared context.
+
+**Flow architecture:**
+
+```
++---------------------------------------------------+
+| 1. Context Aggregation |
+| Read: BOOKS.md, AUDIO.md, CREATIVE.md, |
+| PREFERENCES.md, enrichment answers, |
+| existing taste-profile.json responses, |
+| personality traits (Big Five Openness) |
++---------------------------------------------------+
+| 2. Static Core Question (from P2) |
+| Serve the existing static question first |
+| to establish baseline in that domain |
++---------------------------------------------------+
+| 3. Personalized Follow-Up Generation |
+| LLM generates 1 contextual follow-up using |
+| identity context + previous answer |
+| e.g., "You listed Blade Runner -- what about |
+| its visual language specifically grabbed you?" |
++---------------------------------------------------+
+| 4. Depth Probing (optional, user-initiated) |
+| "Want to go deeper?" button generates |
+| another personalized question that connects |
+| across domains (e.g., music taste <-> visual) |
++---------------------------------------------------+
+| 5. Summary & Synthesis |
+| After core + follow-ups complete, LLM |
+| generates section summary + cross-domain |
+| pattern detection |
++---------------------------------------------------+
+```
+
+**Prompt template for personalized question generation:**
+
+```
+You are a thoughtful interviewer building an aesthetic taste profile.
+You already know the following about this person:
+
+## Identity Context
+{identityContext -- excerpts from BOOKS.md, AUDIO.md, enrichment answers, traits}
+
+## Previous Responses in This Section
+{existingResponses -- Q&A pairs from taste-profile.json for this section}
+
+## Section: {sectionLabel}
+
+Generate ONE follow-up question that:
+1. References something specific from their identity context or previous answers
+2. Probes WHY they prefer what they do, not just WHAT
+3. Feels conversational -- like a friend who knows them asking a natural question
+4. Explores an angle their previous answers haven't covered yet
+5. Is concise (1-2 sentences max)
+
+Do NOT:
+- Ask generic questions that ignore the context
+- Repeat topics already covered in previous responses
+- Use survey language ("On a scale of 1-10...")
+- Ask multiple questions at once
+```
+
+**Example personalized exchanges:**
+
+> **Static (P2):** "Name 3-5 films you consider near-perfect."
+> **User:** "Blade Runner, Stalker, Lost in Translation, Drive, Arrival"
+>
+> **Personalized (P2.5):** "Your BOOKS.md lists several sci-fi titles with themes of isolation and altered perception. Four of your five film picks share that same atmosphere. Is solitude a feature of stories you're drawn to, or is it more about the specific visual treatment of lonely spaces?"
+
+> **Static (P2):** "What artists or albums have had a lasting impact?"
+> **User:** "Radiohead, Boards of Canada, Massive Attack"
+>
+> **Personalized (P2.5):** "All three of those artists layer heavy texture over minimalist structures. Your CREATIVE.md mentions an appreciation for 'controlled complexity.' Does this principle -- density within restraint -- apply to how you think about visual design too?"
+
+#### Data Model -- Where Taste Lives
+
+Taste data lives in **two files** with distinct roles:
+
+**1. Raw questionnaire responses: `data/digital-twin/taste-profile.json`** (existing, extended)
+
+```json
+{
+ "version": "2.0.0",
+ "createdAt": "...",
+ "updatedAt": "...",
+ "sections": {
+ "movies": {
+ "status": "completed",
+ "responses": [
+ {
+ "questionId": "movies-core-1",
+ "answer": "Blade Runner, Stalker, Lost in Translation...",
+ "answeredAt": "...",
+ "source": "static"
+ },
+ {
+ "questionId": "movies-p25-1",
+ "answer": "It's not solitude per se, it's the visual...",
+ "answeredAt": "...",
+ "source": "personalized",
+ "generatedQuestion": "Your BOOKS.md lists several sci-fi titles...",
+ "identityContextUsed": ["BOOKS.md:sci-fi-themes", "taste:movies-core-1"]
+ }
+ ],
+ "summary": "..."
+ },
+ "fashion": { "status": "pending", "responses": [], "summary": null },
+ "digital": { "status": "pending", "responses": [], "summary": null }
+ },
+ "profileSummary": null,
+ "lastSessionAt": null
+}
+```
+
+Changes from v1:
+- `source` field distinguishes static vs personalized questions
+- `generatedQuestion` stores the LLM-generated question text (since personalized questions aren't in the static definition)
+- `identityContextUsed` tracks which identity sources informed the question (for provenance)
+- Two new sections: `fashion`, `digital`
+- Version bumped to 2.0.0
+
+**2. Synthesized aesthetic profile: `data/digital-twin/aesthetics.json`** (planned in P1, populated by P2.5)
+
+```json
+{
+ "version": "1.0.0",
+ "updatedAt": "...",
+ "profile": {
+ "visualStyle": ["brutalist minimalism", "high-contrast neon", "controlled complexity"],
+ "narrativePreferences": ["isolation themes", "slow burn", "ambiguity over resolution"],
+ "musicProfile": ["textural electronica", "atmospheric layering", "functional listening"],
+ "spatialPreferences": ["raw materials", "dramatic light", "intimacy over grandeur"],
+ "culinaryIdentity": ["umami-driven", "improvisational cooking", "experience over formality"],
+ "fashionSensibility": ["monochrome", "natural fibers", "minimal branding"],
+ "digitalAesthetic": ["dark mode", "high information density", "subtle animation"],
+ "antiPatterns": ["visual clutter", "forced symmetry", "saccharine sentimentality"],
+ "corePrinciples": ["density within restraint", "function informing form", "earned complexity"]
+ },
+ "sources": {
+ "tasteQuestionnaire": {
+ "sectionsCompleted": 7,
+ "totalResponses": 28,
+ "lastUpdated": "..."
+ },
+ "enrichment": {
+ "aesthetics": { "questionsAnswered": 5 },
+ "favoriteBooks": { "analyzed": true, "themes": ["existential sci-fi", "systems thinking"] },
+ "favoriteMovies": { "analyzed": true, "themes": ["atmospheric isolation", "neon noir"] },
+ "musicTaste": { "analyzed": true, "themes": ["textural electronica", "ambient"] }
+ },
+ "documents": ["BOOKS.md", "AUDIO.md", "CREATIVE.md", "PREFERENCES.md"]
+ },
+ "crossDomainPatterns": [
+ "Preference for 'controlled complexity' appears across music (layered textures), visual art (minimalist structure with dense detail), architecture (raw materials with precise placement), and food (complex umami built from simple ingredients)",
+ "Anti-preference for overt sentimentality spans film (avoids melodrama), music (dislikes saccharine pop), and design (rejects decorative ornamentation)"
+ ],
+ "genomicCorrelations": {
+ "tasteReceptorGenes": "TAS2R38 status may correlate with bitter-food tolerance preferences",
+ "sensoryProcessing": "Olfactory receptor variants may explain heightened texture sensitivity"
+ }
+}
+```
+
+This file is the **canonical aesthetic profile** referenced by the Identity orchestrator (`identity.json`). It is regenerated whenever taste-profile.json accumulates significant new responses.
+
+#### Implementation Steps
+
+1. **Add 2 new sections** to `TASTE_SECTIONS` in `taste-questionnaire.js`: `fashion` and `digital`, each with 3 core questions and keyword-triggered follow-ups
+2. **Add `aggregateIdentityContext(sectionId)`** to `taste-questionnaire.js` -- reads BOOKS.md, AUDIO.md, CREATIVE.md, PREFERENCES.md, enrichment answers, and existing taste responses to build a context string for the LLM
+3. **Add `generatePersonalizedTasteQuestion(sectionId, existingResponses, identityContext)`** -- calls the active AI provider with the prompt template above, returns a single personalized follow-up question
+4. **Add `POST /api/digital-twin/taste/:section/personalized-question`** route that returns a generated question
+5. **Extend `submitAnswer()`** to accept `source: 'personalized'` and store `generatedQuestion` + `identityContextUsed` metadata
+6. **Add "Go deeper" button** to TasteTab.jsx after each static follow-up cycle completes -- clicking it calls the personalized question endpoint
+7. **Add `generateAestheticsProfile()`** to `taste-questionnaire.js` -- synthesizes all taste-profile.json responses + enrichment data into `aesthetics.json`
+8. **Bump taste-profile.json version** to 2.0.0, migrate existing responses to include `source: 'static'`
+9. **Update TasteTab.jsx** to render personalized questions differently (subtle indicator showing the twin referenced specific context)
+
+#### Prerequisite Relaxation
+
+The original spec listed P1 (Identity orchestrator) as a hard prerequisite. This is relaxed: P2.5 can read identity documents directly from the filesystem (`BOOKS.md`, `AUDIO.md`, etc.) and enrichment data from `meta.json` without needing the orchestrator layer. The orchestrator becomes useful for caching and cross-section queries but is not strictly required for context aggregation.
+
+### P3: Mortality-Aware Goal Tracking
+- Create `data/digital-twin/goals.json`
+- Add `GET/POST/PUT/DELETE /api/digital-twin/identity/goals` routes
+- Birth date input + SSA actuarial table lookup
+- Genome-adjusted life expectancy: weight longevity markers (5 markers: FOXO3A, IGF1R, CETP, IPMK, TP53) and cardiovascular risk markers (5 markers: Factor V, 9p21, Lp(a), LPA aspirin, PCSK9) into adjustment factor
+- Time-horizon calculation: years remaining, healthy years, percent complete
+- Urgency scoring: `urgency = (goalHorizonYears - yearsRemaining) / goalHorizonYears` normalized
+- Goal CRUD with category tagging and milestone tracking
+
+### P4: Identity Tab UI
+- Add `identity` tab to `TABS` constant in `constants.js`
+- Create `IdentityTab.jsx` with dashboard layout (4 summary cards + cross-insights)
+- Create `ChronotypeEditor.jsx` -- schedule visualization and override controls
+- Create `TasteQuestionnaire.jsx` -- section-by-section prompted flow
+- Create `GoalTracker.jsx` -- goal list with urgency heatmap and timeline view
+- Wire sub-routes for deep dives
+
+### P5: Cross-Insights Engine
+- Add `generateCrossInsights(identity)` in identity service
+- Cross-reference genome markers with chronotype, goals, and enrichment data
+- Generate natural-language insight strings (e.g., caffeine + chronotype, longevity + goal urgency)
+- Display on Identity dashboard and inject into CoS context when relevant
+- Consider autonomous job: periodic identity insight refresh
+- Example cross-insights from current marker data:
+ - CLOCK + CRY1 + PER2 -> composite chronotype confidence (3 markers agreeing = high confidence evening/morning)
+ - MTNR1B concern + evening chronotype -> "avoid eating after 8pm -- your melatonin receptor variant impairs late glucose handling"
+ - CYP1A2 slow metabolizer + CLOCK evening -> "caffeine cutoff by noon, not 2pm"
+ - FOXO3A/CETP/IGF1R longevity markers + cardiovascular risk -> adjusted life expectancy for goal urgency
+
+## Extension Roadmap
+
+This roadmap connects brain ideas and the Genome Section Integration project (0e6a0332) into a unified implementation sequence.
+
+### Source Ideas
+- **Brain idea 608dc733**: "Prompting Aesthetic Taste Docs via Digital Twin" -- use the twin's existing knowledge to generate personalized aesthetic preference questions
+- **Brain idea 284dd487**: "Genome Types & Chronotype Trait" -- derive chronotype from 5 sleep/circadian markers + behavioral data
+- **Project 0e6a0332**: "Genome Section Integration" -- unify genome data with Identity page architecture
+
+### Phase Dependency Graph
+
+```
+P1: Identity Orchestrator & Chronotype ---- (brain idea 284dd487)
+ | Creates identity.json, chronotype.json,
+ | identity service, derivation from 5 sleep markers
+ |
+ +-> P2.5: Personalized Taste Prompting --- (brain idea 608dc733)
+ | Uses identity context to generate smart taste questions
+ | Enhances existing TasteTab with twin-aware follow-ups
+ |
+ +-> P3: Mortality-Aware Goal Tracking
+ | Birth date + genome longevity/cardio markers -> life expectancy
+ | Urgency scoring for prioritized goal management
+ |
+ +-> P4: Identity Tab UI
+ Dashboard with summary cards for all 4 sections
+ Sub-routes for chronotype, taste, goals deep dives
+ |
+ +-> P5: Cross-Insights Engine
+ Reads all sections, generates natural-language insights
+ Injects identity context into CoS agent briefings
+```
+
+### Implementation Priority
+1. **P1** -- Foundation: nothing else works without the orchestrator
+2. **P2.5** -- Quick win: enhances existing Taste tab with minimal new infrastructure
+3. **P3** -- New feature: mortality-aware goals need genome data flowing through identity service
+4. **P4** -- UI: renders what P1-P3 produce
+5. **P5** -- Polish: cross-entity reasoning requires all sections populated
+
+## Data Flow
+
+```
+User uploads 23andMe -> genome.json (117 markers, 32 categories)
+ |
+Identity service reads 5 sleep markers + 2 caffeine markers
+ |
+Derives chronotype.json (+ behavioral input from daily_routines enrichment)
+ |
+Twin reads identity context -> generates personalized taste questions (P2.5)
+ |
+User completes taste questionnaire -> taste-profile.json -> aesthetics.json
+ |
+LLM analyzes books/movies/music docs -> seeds aesthetic profile themes
+ |
+User sets birth date -> goals.json (life expectancy from actuarial + 10 genome markers)
+ |
+Cross-insights engine reads all 4 sections -> generates natural-language insights
+ |
+Identity tab renders unified dashboard with summary cards + insights
+ |
+CoS injects identity context into agent briefings when relevant
+```
+
+## Files to Create/Modify
+
+**New files:**
+- `data/digital-twin/identity.json` -- orchestrator metadata
+- `data/digital-twin/chronotype.json` -- derived chronotype profile
+- `data/digital-twin/aesthetics.json` -- taste profile
+- `data/digital-twin/goals.json` -- mortality-aware goals
+- `server/services/identity.js` -- identity orchestration service
+- `server/routes/identity.js` -- API routes
+- `server/lib/identityValidation.js` -- Zod schemas
+- `client/src/components/digital-twin/tabs/IdentityTab.jsx` -- dashboard
+- `client/src/components/digital-twin/identity/ChronotypeEditor.jsx`
+- `client/src/components/digital-twin/identity/TasteQuestionnaire.jsx`
+- `client/src/components/digital-twin/identity/GoalTracker.jsx`
+- `client/src/components/digital-twin/identity/CrossInsights.jsx`
+
+**Modified files:**
+- `client/src/components/digital-twin/constants.js` -- add Identity tab
+- `client/src/pages/DigitalTwin.jsx` -- add Identity tab rendering
+- `client/src/services/api.js` -- add identity API methods
+- `server/index.js` -- mount identity routes
+- `server/services/taste-questionnaire.js` -- add `generatePersonalizedTasteQuestion()` using identity context (P2.5)
+- `client/src/components/digital-twin/tabs/TasteTab.jsx` -- wire personalized question generation (P2.5)
+
+## Design Decisions
+
+1. **Separate data files per section** (not one giant file) -- each section has independent update cadence and the genome file (82KB) is already large
+2. **Derivation over duplication** -- chronotype reads from genome.json at query time rather than copying marker data. Identity service is the join layer
+3. **Progressive disclosure** -- Identity tab shows summary cards; deep dives are sub-routes, not modals (per CLAUDE.md: all views must be deep-linkable)
+4. **LLM-assisted but user-confirmed** -- aesthetic themes extracted by LLM from media lists are suggestions, not gospel. User confirms/edits
+5. **No new dependencies** -- uses existing Zod, Express, React, Lucide stack
+6. **Genome data stays read-only** -- identity service reads genome markers but never writes to genome.json
+7. **Taste data consolidation** -- P2 created `taste-profile.json` (5 sections). P2.5 adds twin-aware personalized questions. Long-term, taste data migrates into `aesthetics.json` as the canonical aesthetic profile, with taste-profile.json as the raw questionnaire responses
+8. **Weighted chronotype confidence** -- 5 sleep markers weighted by specificity: CRY1 (strongest DSPD signal) > CLOCK (evening tendency) > PER2 (circadian period) > MTNR1B (melatonin coupling) > DEC2 (duration, not phase). Behavioral data from daily_routines enrichment gets equal weight to genetic composite
diff --git a/docs/research/kalshibot-health-check-2026-02-17.md b/docs/research/kalshibot-health-check-2026-02-17.md
deleted file mode 100644
index 46d0f86c..00000000
--- a/docs/research/kalshibot-health-check-2026-02-17.md
+++ /dev/null
@@ -1,175 +0,0 @@
-# Kalshibot Health Check Analysis β 2026-02-17
-
-## Summary
-
-**Status: DEGRADED** β 0% win rate across 3 trades, -$148.14 total loss on 2026-02-16. All 3 live trades settled at $0 (complete loss of cost basis). Shadow gamma-scalper posted +$46 on 1 trade (100% win rate). Current balance: $1,024.51.
-
----
-
-## Trade-by-Trade Analysis
-
-### Trade 1: Settlement Sniper β KXBTC-26FEB1611-B67375 (-$42.77)
-
-- **Ticker**: B67375 bracket ($67,250-$67,500)
-- **Side**: YES (betting BTC settles in this bracket)
-- **Entry**: 200 contracts @ 21c ($42.77 + $1.69 fee) at 15:54 UTC
-- **Settlement**: YES = $0 at 16:00 UTC (BTC was NOT in this bracket)
-- **Loss**: -$42.77 (100% of cost basis, 4.2% of balance)
-
-**Root cause**: The model estimated >33% fair probability for this bracket (21c + 12% edge). With 200 contracts (the configured max), Kelly sizing put $42 at risk on a single binary outcome. BTC settled outside this range, zeroing the position.
-
-**Key issue**: `settlementRideThreshold: 0.40` may have prevented the 60s exit window from triggering. If the model still showed 40%+ edge at t-60s, the position rode to $0 instead of exiting with a partial loss.
-
-### Trade 2: Coinbase Fair Value β KXBTC-26FEB1611-B67625 (-$52.79)
-
-- **Ticker**: B67625 bracket ($67,500-$67,750)
-- **Side**: NO (betting BTC does NOT settle in this bracket)
-- **Entry**: 186 contracts @ 28c ($52.79 + $2.57 fee) at 15:56 UTC
-- **Settlement**: NO = $0 at 16:00 UTC (BTC WAS in this bracket β NO bet lost)
-- **Loss**: -$52.79 (100% of cost basis, 5.2% of balance)
-
-**Root cause**: The strategy used a lowered `edgeThreshold: 0.20` (default is 0.25) and wider `maxSecondsToSettlement: 300` (default is 180). Entry at 3m35s before settlement with a 20% edge threshold allowed a signal that wouldn't have passed at the default 25% threshold. The NO side was wrong β BTC landed in this bracket.
-
-**Position sizing note**: 186 contracts @ 28c = $52, exceeding the 3% `maxBetPct` of ~$30. The `calculatePositionSize` method may not be correctly capping by `maxBetPct`.
-
-### Trade 3: Coinbase Fair Value β KXBTC-26FEB1612-B67625 (-$52.58)
-
-- **Ticker**: B67625 bracket ($67,500-$67,750), next hour
-- **Side**: YES (betting BTC settles in this bracket)
-- **Entry**: 141 contracts across 3 fills @ 37-38c ($52.58 + $2.32 fee) at 16:56 UTC
-- **Settlement**: YES = $0 at 17:00 UTC (BTC was NOT in this bracket)
-- **Loss**: -$52.58 (100% of cost basis, 5.1% of balance)
-
-**Root cause**: Same bracket, opposite side, next hour. BTC moved away from $67,500-$67,750 between 16:00 and 17:00. Higher entry price (37-38c) indicates greater model confidence, but the thesis was still wrong.
-
-### Shadow Trade: Gamma Scalper β KXBTC-26FEB1612-B67875 (+$46.00)
-
-- **Ticker**: B67875 bracket ($67,750-$68,000)
-- **Side**: NO (betting BTC does NOT settle in this bracket)
-- **Entry**: 50 contracts @ 8c ($4.00 + $0.26 fee) at 16:57 UTC
-- **Settlement**: NO = $1.00 at 17:00 UTC ($50 proceeds, +$46 profit)
-- **Edge reported**: 77.1%
-
-**Why it outperformed**:
-1. Tiny risk: $4 total cost vs $42-52 for live strategies
-2. Asymmetric payoff: 8c entry for $1 payout = 12.5x return
-3. Strong signal: 77.1% edge vs 12-20% threshold for live strategies
-4. Correct thesis: BTC was not in the $67,750-$68,000 range
-
----
-
-## Systemic Issues Identified
-
-### 1. Position Sizing Too Aggressive for Binary Bracket Outcomes
-
-All 3 live trades risked $42-52 each (4-5% of balance). Bracket markets settle at $0 or $1 β there's no partial recovery. Current `maxBetPct` settings (5% sniper, 3% fair-value) allow catastrophic per-trade losses.
-
-### 2. Coinbase Fair Value Config Deviates from Safer Defaults
-
-| Parameter | Current | Default | Risk Impact |
-|-----------|---------|---------|-------------|
-| `edgeThreshold` | 0.20 | 0.25 | Allows noisier signals |
-| `maxSecondsToSettlement` | 300 | 180 | Enters too early, less certain |
-| `exitEdgeThreshold` | 0.08 | 0.10 | Holds losing positions longer |
-| `maxPositions` | 3 | 2 | More concurrent risk |
-
-### 3. No Per-Window Exposure Cap
-
-Trades 1 and 2 both targeted the 16:00 UTC settlement window. Combined exposure: $95 (9.3% of balance) on a single 15-minute interval. No mechanism caps aggregate risk per settlement window.
-
-### 4. Settlement Ride Exception May Amplify Losses
-
-The sniper's `settlementRideThreshold: 0.40` can override the forced exit at t-60s. In bracket markets where the probability model can be persistently wrong (model shows 40% edge but the bracket misses), this turns a possible small-loss exit into a guaranteed 100% loss.
-
-### 5. Gamma-Scalper Live Execution Gap (Root Cause Confirmed)
-
-Gamma-scalper is `enabled: true` in config but was blocked from live execution by the **one-position-per-settlement-window** rule in `simulation-engine.js` (lines 773-798). The engine evaluates strategies in config order: settlement-sniper β coinbase-fair-value β momentum-rider β gamma-scalper. By the time gamma-scalper generated its B67875 signal at 16:57 UTC, the coinbase-fair-value strategy had already placed a position (B67625 YES) in the 17:00 UTC settlement window, triggering the cross-position conflict check.
-
-**Code path**: `simulation-engine.js:773-798` β when a buy signal arrives, the engine checks if any existing position or pending reservation shares the same `close_time`. If so, the signal is rejected with `"settlement window conflict"`. Since gamma-scalper evaluates last in the strategy loop (`simulation-engine.js:680`), it always loses to earlier strategies.
-
-**Why the shadow trade succeeded**: Shadow evaluation (`simulation-engine.js:863-925`) runs against `shadowState.positions`, which is separate from live positions. The shadow state had no positions in the 17:00 window, so the gamma-scalper signal passed.
-
-**Fix required**: Strategy evaluation order should prioritize lower-risk strategies (gamma-scalper at $4/trade) over higher-risk ones ($50/trade), or the engine should collect all signals first and rank them before executing.
-
----
-
-## Recommended Parameter Changes
-
-### Immediate (config.json changes only)
-
-```json
-{
- "strategies": {
- "settlement-sniper": {
- "params": {
- "maxBetPct": 0.03,
- "maxContracts": 100,
- "settlementRideThreshold": 1.0
- }
- },
- "coinbase-fair-value": {
- "params": {
- "edgeThreshold": 0.25,
- "exitEdgeThreshold": 0.10,
- "maxSecondsToSettlement": 180,
- "maxPositions": 2
- }
- },
- "gamma-scalper": {
- "params": {
- "maxPositions": 3
- }
- }
- }
-}
-```
-
-**Rationale per change**:
-
-1. **sniper `maxBetPct` 0.05 -> 0.03**: Cap single-trade risk at 3%. Trade 1 would have risked ~$21 instead of $42.
-2. **sniper `maxContracts` 200 -> 100**: Hard cap on position size. Combined with lower `maxBetPct`, prevents outsized bracket bets.
-3. **sniper `settlementRideThreshold` 0.40 -> 1.0**: Effectively disables settlement riding. Forces positions to exit at t-60s instead of riding to $0. Can be re-enabled after more shadow testing validates the feature.
-4. **fair-value `edgeThreshold` 0.20 -> 0.25**: Restore default. Requires 25% divergence before entry, filtering out Trade 2's 20% signal.
-5. **fair-value `exitEdgeThreshold` 0.08 -> 0.10**: Exit sooner when thesis weakens.
-6. **fair-value `maxSecondsToSettlement` 300 -> 180**: Restore default. Prevents entries at 3m+ before settlement where vol estimates are noisier.
-7. **fair-value `maxPositions` 3 -> 2**: Reduce concurrent risk exposure.
-8. **gamma-scalper `maxPositions` 2 -> 3**: Give the proven low-risk strategy more room to deploy.
-
-### Post-Analysis Config Audit (2026-02-17)
-
-After the initial health check, some parameters were applied to `config.json` but several were applied incorrectly or missed:
-
-| Parameter | Health Check Target | Current Config | Status |
-|-----------|-------------------|----------------|--------|
-| sniper `maxBetPct` | 0.02 | 0.02 | Applied (2026-02-18) |
-| sniper `maxContracts` | 100 | 100 | Applied |
-| sniper `settlementRideThreshold` | 1.0 | 1.0 | Applied |
-| fair-value `edgeThreshold` | 0.25 | 0.25 | Applied (2026-02-18) |
-| fair-value `exitEdgeThreshold` | 0.10 | 0.10 | Applied |
-| fair-value `maxSecondsToSettlement` | 180 | 180 | Applied (2026-02-18) |
-| fair-value `maxBetPct` | 0.02 | 0.02 | Applied (2026-02-18) |
-| fair-value `maxPositions` | 2 | 2 | Applied |
-| gamma-scalper `maxPositions` | 3 | 3 | Applied (2026-02-18) |
-| gamma-scalper `maxEdgeSanity` | 0.95 | 0.95 | Added (2026-02-18) β per-strategy override |
-
-### Code Changes Applied (2026-02-18)
-
-1. **Strategy evaluation order by risk**: In `simulation-engine.js:634`, enabled strategies are now sorted by `maxBetPct` ascending (cheapest first). Gamma-scalper ($4/trade) gets window priority over fair-value ($50/trade). This would have allowed the +$46 gamma-scalper trade to execute live on 2026-02-16.
-2. **Per-strategy edge sanity override**: In `simulation-engine.js:782`, the edge sanity cap now checks `strategy.params.maxEdgeSanity` before falling back to the global `risk.maxEdgeSanity`. Gamma-scalper's OTM bracket strategy inherently produces high-edge signals (77% in the shadow trade) that were blocked by the global 0.85 cap. Its per-strategy cap is now 0.95.
-
-### Code Changes Previously Needed (Kalshibot repo)
-
-1. ~~**Strategy evaluation order by risk** (CRITICAL): In `simulation-engine.js:680`, the strategy loop evaluates in config order. Since only one position per settlement window is allowed (line 773-798), the first strategy to claim a window wins. Change the loop to sort enabled strategies by `maxBetPct` ascending (cheapest first), so gamma-scalper ($4/trade) gets priority over fair-value ($50/trade). This single change would have allowed the +$46 gamma-scalper trade to execute live.~~ DONE
-2. **Per-window exposure cap**: Already implemented at `simulation-engine.js:800-815` with `maxExposurePerWindow: 75`. This was added after the initial analysis β verify it's working correctly.
-3. **Position size audit**: Verify `calculatePositionSize` in `base-strategy.js` correctly enforces `maxBetPct` β Trade 2's $52 cost exceeded the 3% cap of ~$30.
-
----
-
-## Impact Estimate
-
-If these parameter changes had been active on 2026-02-16:
-- **Trade 1**: ~$21 loss instead of $42 (maxContracts: 100, maxBetPct: 0.03) β already applied
-- **Trade 2**: Filtered out entirely (edgeThreshold 0.25 would reject the 20% signal)
-- **Trade 3**: Likely filtered or reduced (tighter maxSecondsToSettlement: 180 blocks 4m-early entries)
-- **Gamma-scalper**: With strategy-order-by-risk, would have claimed the 17:00 window first β +$46 live
-- **Estimated day**: -$21 + $46 = **+$25 net** instead of -$148 β a $173 improvement
diff --git a/docs/research/pumpfun-data-sources.md b/docs/research/pumpfun-data-sources.md
deleted file mode 100644
index 3c9fe87a..00000000
--- a/docs/research/pumpfun-data-sources.md
+++ /dev/null
@@ -1,459 +0,0 @@
-# Pump.fun Launch Tracking Engine: Data Source Evaluation
-
-**Brain Project**: 467fbe07 β Pump.fun Launch Tracking Engine
-**Date**: 2026-02-16
-**Deadline**: 2026-02-20
-**Objective**: Evaluate Helius, Birdeye, and pump.fun APIs for rate limits, auth, and schemas
-
----
-
-## Executive Summary
-
-Three primary data source categories were evaluated for the Pump.fun Launch Tracking Engine:
-
-1. **Helius** β Solana-native RPC/infrastructure with Enhanced APIs, webhooks, and gRPC streaming
-2. **Birdeye** β DeFi analytics platform with rich token/price/trade REST APIs
-3. **pump.fun Direct + Third-Party Indexers** β pump.fun's own frontend APIs plus Bitquery/bloXroute GraphQL indexers
-
-**Recommendation**: Use Helius (Developer tier, $49/mo) as the primary real-time data source for new token detection and transaction monitoring. Supplement with Birdeye (Starter tier, $99/mo) for enriched token analytics, price history, and holder data. Use pump.fun direct APIs sparingly for metadata not available elsewhere.
-
----
-
-## 1. Helius
-
-### Overview
-Solana-native RPC and API platform. Best-in-class for raw blockchain data, real-time streaming, and transaction parsing on Solana.
-
-### Authentication
-- API key appended as query parameter: `?api-key=YOUR_KEY`
-- Keys managed via [Helius Dashboard](https://dashboard.helius.dev)
-- SDK available: `@helius-labs/helius-sdk` (npm)
-
-### Pricing & Rate Limits
-
-| Plan | Cost/mo | Credits/mo | RPC RPS | DAS API RPS | Enhanced API RPS | WebSockets |
-|------|---------|-----------|---------|-------------|-----------------|------------|
-| Free | $0 | 1M | 10 | 2 | 2 | Standard only |
-| Developer | $49 | 10M | 50 | 10 | 10 | Standard only |
-| Business | $499 | 100M | 200 | 50 | 50 | Enhanced |
-| Professional | $999 | 200M | 500 | 100 | 100 | Enhanced + LaserStream |
-
-### Key APIs for Pump.fun Tracking
-
-**Enhanced Transactions API**
-- `POST https://api-mainnet.helius-rpc.com/v0/transactions?api-key=KEY` β parse up to 100 signatures per request
-- `GET https://api-mainnet.helius-rpc.com/v0/addresses/{address}/transactions?api-key=KEY` β address history with pagination
-- Parses raw Solana transactions into human-readable format
-- Decodes instruction data, token transfers, balance changes
-- Response includes: `description`, `type`, `source`, `fee`, `feePayer`, `signature`, `slot`, `timestamp`, `nativeTransfers`, `tokenTransfers`, `accountData`, `events`
-- Filter by pump.fun program ID: `6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`
-- Commitment levels: `finalized` (default) or `confirmed`
-- Error responses: 400 (bad request), 401 (auth), 429 (rate limit), 500/503/504 (server)
-
-**Webhooks**
-- Push-based event delivery for on-chain events
-- Configurable filters: `TOKEN_MINT`, account-specific, program-specific
-- Can monitor pump.fun program for new token creates and trades
-- Eliminates polling β server receives events as they happen
-
-**gRPC Streaming (LaserStream)**
-- Real-time account and transaction streams
-- Filter by program owner for pump.fun bonding curve accounts
-- Commitment level: `CONFIRMED`
-- Tracks: operation type, user/fee payer, signatures, timestamps, balance changes
-- **Note**: Professional plan only for mainnet gRPC
-
-**DAS (Digital Asset Standard) API**
-- Token metadata, ownership, and collection queries
-- Useful for enriching token data post-detection
-
-### Response Schema (Enhanced Transaction)
-```json
-{
- "description": "string",
- "type": "SWAP|TOKEN_MINT|TRANSFER|...",
- "source": "PUMP_FUN|RAYDIUM|...",
- "fee": 5000,
- "feePayer": "pubkey",
- "signature": "txid",
- "timestamp": 1700000000,
- "nativeTransfers": [{ "fromUserAccount": "...", "toUserAccount": "...", "amount": 1000000 }],
- "tokenTransfers": [{ "fromTokenAccount": "...", "toTokenAccount": "...", "tokenAmount": 1000, "mint": "..." }],
- "accountData": [{ "account": "...", "nativeBalanceChange": -5000, "tokenBalanceChanges": [...] }]
-}
-```
-
-### Strengths
-- Lowest latency for new token detection (webhooks + gRPC)
-- Native pump.fun program filtering
-- Enhanced transaction parsing reduces client-side logic
-- Staked connections on all paid plans for high tx success
-- Well-documented SDK
-
-### Limitations
-- gRPC/LaserStream requires Professional ($999/mo) for mainnet
-- Enhanced WebSocket metering (3 credits/0.1MB) for new users
-- Raw blockchain data β no pre-built analytics (no OHLCV, no market cap aggregation)
-
----
-
-## 2. Birdeye
-
-### Overview
-DeFi analytics platform with comprehensive REST APIs for token data, pricing, trades, OHLCV, and wallet analytics. Covers Solana and 30+ other chains.
-
-### Authentication
-- API key via header: `X-API-KEY: YOUR_KEY`
-- Keys managed via [Birdeye Dashboard](https://bds.birdeye.so)
-- Optional `chain` parameter defaults to Solana
-
-### Pricing & Rate Limits
-
-| Plan | Cost/mo | Compute Units | Global RPS | WebSockets |
-|------|---------|--------------|-----------|------------|
-| Standard (Free) | $0 | 30K | 1 | No |
-| Lite | $39 | 1.5M | 15 | No |
-| Starter | $99 | 5M | 15 | No |
-| Premium | $199 | 15M | 50 | No |
-| Premium Plus | $250 | 20M | 50 | 500 conns |
-| Business (B-05) | $499 | 50M | 100 | 2000 conns |
-| Business | $699 | 100M | 100 | Yes |
-
-**Per-Endpoint Rate Limits** (within global account limit):
-
-| Endpoint | Path | Max RPS |
-|----------|------|---------|
-| Price (single) | `/defi/price` | 300 |
-| Price (multi) | `/defi/multi_price` | 300 |
-| Price (historical) | `/defi/history_price` | 100 |
-| Token Overview | `/defi/token_overview` | 300 |
-| Token Security | `/defi/token_security` | 150 |
-| Token List v3 | `/defi/v3/token/list` | 100 |
-| Trades (token) | `/defi/txs/token` | 100 |
-| Trades (pair) | `/defi/txs/pair` | 100 |
-| OHLCV | `/defi/ohlcv` | 100 |
-| Wallet Portfolio | varies | 30 rpm |
-
-### Key APIs for Pump.fun Tracking
-
-**Token Overview** (`/defi/token_overview`)
-- Market cap, liquidity, volume, price change, holder count
-- Single call returns comprehensive token analytics
-
-**Token Security** (`/defi/token_security`)
-- Rug-pull risk indicators, mint authority, freeze authority
-- Critical for filtering high-risk launches
-
-**Price APIs** (`/defi/price`, `/defi/history_price`)
-- Real-time and historical pricing in SOL/USD
-- Multi-token batch pricing supported
-
-**Trade APIs** (`/defi/txs/token`)
-- Recent trades with buy/sell side, amounts, timestamps
-- Pair-level trade history
-
-**OHLCV** (`/defi/ohlcv`)
-- Candlestick data at configurable intervals
-- Useful for charting and trend detection
-
-**Token List** (`/defi/v3/token/list`)
-- Sortable by volume, market cap, price change
-- Filter by timeframe for trending tokens
-
-### Response Schema (Token Overview)
-```json
-{
- "address": "mint_address",
- "name": "Token Name",
- "symbol": "TKN",
- "decimals": 9,
- "price": 0.00123,
- "priceChange24hPercent": 150.5,
- "volume24h": 500000,
- "marketCap": 1200000,
- "liquidity": 50000,
- "holder": 2500,
- "supply": 1000000000,
- "logoURI": "https://...",
- "extensions": { "website": "...", "twitter": "..." }
-}
-```
-
-### Response Schema (Price API β `GET /defi/price?address=MINT`)
-```json
-{
- "success": true,
- "data": {
- "value": 0.38622,
- "updateUnixTime": 1745058945,
- "updateHumanTime": "2025-04-19T10:35:45",
- "priceChange24h": 1.93,
- "priceInNative": 0.00277,
- "liquidity": 10854103.37
- }
-}
-```
-
-### Strengths
-- Richest analytics out of the box (market cap, liquidity, security scores)
-- Pre-computed OHLCV eliminates aggregation logic
-- Token security endpoint critical for filtering scams
-- Batch pricing for monitoring multiple tokens
-- Clean REST API, easy to integrate
-
-### WebSocket: New Token Listing Stream
-
-Available on Premium Plus ($250/mo) and above. Directly relevant for pump.fun launch detection.
-
-- **URL**: `wss://public-api.birdeye.so/socket/solana?x-api-key=YOUR_KEY`
-- **Headers**: `Origin: ws://public-api.birdeye.so`, `Sec-WebSocket-Protocol: echo-protocol`
-- **Subscribe**: `{ "type": "SUBSCRIBE_TOKEN_NEW_LISTING", "meme_platform_enabled": true, "sources": ["pump_dot_fun"] }`
-- **CU cost**: 0.08 CU per byte
-
-Response schema:
-```json
-{
- "type": "TOKEN_NEW_LISTING_DATA",
- "data": {
- "address": "BkQfwVktcbWmxePJN5weHWJZgReWbiz8gzTdFa2w7Uds",
- "decimals": 6,
- "name": "Worker Cat",
- "symbol": "$MCDCAT",
- "liquidity": "12120.155172280874",
- "liquidityAddedAt": 1720155863
- }
-}
-```
-
-Supports `min_liquidity`/`max_liquidity` filters and 100+ DEX source filters including Raydium, Orca, Meteora, and pump.fun.
-
-### Compute Unit Costs (Key Endpoints)
-
-| Endpoint | CU Cost | Notes |
-|----------|---------|-------|
-| Token Price | 10 | Cheapest price check |
-| Token Metadata | 5 | Very low cost |
-| Token List v3 | 100 | Higher cost for list queries |
-| Trades (token) | 10 | Affordable for trade monitoring |
-| OHLCV | 40 | Moderate |
-| Token New Listing (REST) | 80 | One-shot listing check |
-| WS: New Listing | 0.08/byte | Streaming cost scales with data |
-| WS: Price | 0.003/byte | Very affordable streaming |
-| WS: Transactions | 0.0004/byte | Cheapest stream |
-
-### Limitations
-- No push-based event delivery on tiers below Premium Plus (polling only)
-- WebSocket access requires Premium Plus ($250/mo) minimum
-- New token detection via REST has inherent latency β tokens must be indexed first
-- Wallet endpoints severely rate-limited (30 rpm)
-- Compute unit costs can escalate with heavy usage
-- CU costs subject to change without notice
-
----
-
-## 3. pump.fun Direct APIs + Third-Party Indexers
-
-### 3a. pump.fun Frontend API (Direct)
-
-### Overview
-pump.fun exposes several undocumented/semi-official API services. These are reverse-engineered from the frontend and may change without notice.
-
-### Base URLs
-
-| Service | URL | Purpose |
-|---------|-----|---------|
-| Frontend API v3 | `https://frontend-api-v3.pump.fun` | Token data, listings |
-| Advanced Analytics v2 | `https://advanced-api-v2.pump.fun` | Analytics, rankings |
-| Market API | `https://market-api.pump.fun` | Market data |
-| Profile API | `https://profile-api.pump.fun` | User profiles |
-| Swap API | `https://swap-api.pump.fun` | Token swaps |
-| Volatility API v2 | `https://volatility-api-v2.pump.fun` | Volatility metrics |
-
-### Authentication
-- JWT Bearer token: `Authorization: Bearer `
-- Required headers: `Origin: https://pump.fun`, `Accept: application/json`
-- Rate limit headers in responses: `x-ratelimit-limit`, `x-ratelimit-remaining`, `x-ratelimit-reset`
-
-### Key Capabilities
-- **483 documented endpoints** across all API versions
-- Token creation details, bonding curve status, graduation tracking
-- Direct access to pump.fun-specific metadata not available elsewhere
-- Creator profiles and reputation data
-
-### Key V3 Endpoints
-- `GET /coins/latest` β latest token launches
-- `GET /coins/{mint}` β token details by mint address
-- `GET /trades/latest` β latest trades across all tokens
-- `GET /trades/token/{mint}` β trades for specific token
-
-### Observed Rate Limits
-- ~20 requests per minute (RPM) across all endpoints
-- Rate limit headers in responses: `x-ratelimit-limit`, `x-ratelimit-remaining`, `x-ratelimit-reset`
-- HTTP 429 on exceeded limits
-- Recommended: exponential backoff with max 3 retries
-
-### Limitations
-- **Undocumented/unofficial** β endpoints can break without warning
-- JWT auth requires mimicking browser authentication flow
-- Rate limits are restrictive (~20 RPM) and undocumented officially
-- No SLA or support
-- Legal gray area for automated access
-- WebSocket support listed as "coming soon" β not yet available
-
----
-
-### 3b. Bitquery (GraphQL Indexer)
-
-### Overview
-Third-party GraphQL indexer with dedicated pump.fun query support. Real-time subscriptions for new tokens, trades, and bonding curve events.
-
-### Authentication
-- API key via header or query parameter
-- Free tier available via [Bitquery IDE](https://ide.bitquery.io)
-
-### Pricing
-
-| Plan | Cost | Points | RPS | Streams |
-|------|------|--------|-----|---------|
-| Developer (Free) | $0 | 1,000 | 10 req/min | 2 |
-| Commercial | Custom | Custom | Custom | Custom |
-
-### Key APIs for Pump.fun Tracking
-- **Token creation subscriptions** β real-time stream of new pump.fun launches
-- **Trade subscriptions** β buy/sell with amounts and prices
-- **Bonding curve status** β track graduation progress
-- **ATH market cap** β all-time high calculations
-- **Top traders/holders** β wallet analytics
-- **Creator reputation** β all tokens by a creator address
-
-### GraphQL Query Example (New Token Subscription)
-```graphql
-subscription {
- Solana {
- Instructions(
- where: {
- Instruction: {
- Program: {
- Address: { is: "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P" }
- }
- }
- Transaction: { Result: { Success: true } }
- }
- ) {
- Transaction { Signature }
- Instruction {
- Accounts { Address Token { Mint Owner } }
- Program { Method }
- }
- Block { Time }
- }
- }
-}
-```
-
-### Limitations
-- Free tier extremely limited (1,000 points, 10 req/min, 2 streams)
-- Commercial pricing requires sales contact β no self-serve
-- Points-based billing is opaque β hard to predict costs
-- GraphQL complexity can lead to unexpected point consumption
-
----
-
-### 3c. bloXroute (Streaming)
-
-### Overview
-Specializes in low-latency Solana data streaming with dedicated pump.fun channels.
-
-### Key Endpoints
-- `GetPumpFunNewTokensStream` β real-time new token events
-- `GetPumpFunSwapsStream` β real-time swap monitoring
-- `GetPumpFunAMMSwapsStream` β AMM swap events post-graduation
-
-### Limitations
-- Pricing not publicly documented
-- Primarily targets high-frequency trading use cases
-- Overkill for analytics/tracking use case
-
----
-
-## Comparison Matrix
-
-| Criteria | Helius | Birdeye | pump.fun Direct | Bitquery |
-|----------|--------|---------|-----------------|----------|
-| **New token detection latency** | ~1s (webhook/gRPC) | 5-30s (REST) / ~2s (WS) | Unknown | ~2-5s (subscription) |
-| **Real-time streaming** | gRPC + WebSocket | WS w/ pump.fun filter ($250+) | No | GraphQL subscriptions |
-| **Token analytics** | Raw tx data only | Rich (mcap, vol, security) | Basic metadata | Rich (GraphQL) |
-| **OHLCV / Charts** | No | Yes | No | Yes |
-| **Security scoring** | No | Yes | No | Partial |
-| **Holder data** | Via DAS API | Via token overview | No | Yes (top 10) |
-| **Auth complexity** | API key (simple) | API key (simple) | JWT (complex) | API key (simple) |
-| **Stability / SLA** | Production-grade | Production-grade | No SLA, may break | Production-grade |
-| **Min. useful tier** | Developer ($49) | Starter ($99) | Free (risky) | Commercial ($$?) |
-| **Pump.fun specific** | Program filter | General DeFi | Native | Dedicated queries |
-| **SDK / DX** | Excellent (npm SDK) | REST (straightforward) | None | GraphQL IDE |
-
----
-
-## Recommended Architecture
-
-```
- +------------------+
- | Helius ($49) |
- | Webhooks/WS |
- +--------+---------+
- |
- New token events
- Raw transactions
- |
- v
- +------------------+
- | Tracking Engine |
- | (PortOS app) |
- +--------+---------+
- |
- Token enrichment
- Analytics queries
- |
- v
- +------------------+
- | Birdeye ($99) |
- | REST API |
- +------------------+
- - Market cap, volume
- - Security scores
- - OHLCV data
- - Holder counts
-```
-
-### Phase 1 (MVP): Helius Developer ($49/mo)
-- Webhook listening for pump.fun program transactions
-- Detect new token creates via `TOKEN_MINT` events
-- Parse creator address, token mint, initial supply
-- Store in PortOS data layer
-
-### Phase 2 (Enrichment): Add Birdeye Starter ($99/mo)
-- Enrich detected tokens with market data
-- Token security scoring for scam filtering
-- OHLCV data for trend detection
-- Track high-performing tokens over time
-
-### Phase 3 (Analytics): Evaluate Bitquery or pump.fun direct
-- Creator reputation analysis
-- Sniper account inventory
-- Launch prediction model inputs
-
-**Total estimated cost**: $148/mo for Phase 1+2
-
----
-
-## Next Steps
-
-1. **Create Helius account** and generate API key
-2. **Set up webhook** for pump.fun program (`6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P`)
-3. **Create Birdeye account** and generate API key
-4. **Build proof-of-concept** endpoint in PortOS that:
- - Receives Helius webhook events
- - Extracts new token mint + creator
- - Enriches via Birdeye token overview
- - Persists to `data/pumpfun/tokens.json`
-5. **Validate latency** β measure time from on-chain creation to detection
diff --git a/package-lock.json b/package-lock.json
index 2d682b2c..8c76c5f1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "portos",
- "version": "0.14.21",
+ "version": "0.15.15",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "portos",
- "version": "0.14.21",
+ "version": "0.15.15",
"license": "MIT",
"workspaces": [
"packages/*",
@@ -14,14 +14,14 @@
"client"
],
"dependencies": {
- "express": "^5.2.1",
- "portos-ai-toolkit": "github:atomantic/portos-ai-toolkit#ac13168156f81c90dab57702c0de3f97de7970a9"
+ "express": "^4.21.2",
+ "portos-ai-toolkit": "^0.5.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"concurrently": "^8.2.2",
- "pm2": "^6.0.14",
+ "pm2": "^5.4.3",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^6.0.6"
@@ -29,7 +29,7 @@
},
"client": {
"name": "portos-client",
- "version": "0.14.21",
+ "version": "0.15.15",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
@@ -1648,10 +1648,9 @@
}
},
"node_modules/@pm2/agent": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.1.1.tgz",
- "integrity": "sha512-0V9ckHWd/HSC8BgAbZSoq8KXUG81X97nSkAxmhKDhmF8vanyaoc1YXwc2KVkbWz82Rg4gjd2n9qiT3i7bdvGrQ==",
- "dev": true,
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.4.tgz",
+ "integrity": "sha512-n7WYvvTJhHLS2oBb1PjOtgLpMhgImOq8sXkPBw6smeg9LJBWZjiEgPKOpR8mn9UJZsB5P3W4V/MyvNnp31LKeA==",
"license": "AGPL-3.0",
"dependencies": {
"async": "~3.2.0",
@@ -1659,11 +1658,12 @@
"dayjs": "~1.8.24",
"debug": "~4.3.1",
"eventemitter2": "~5.0.1",
- "fast-json-patch": "^3.1.0",
+ "fast-json-patch": "^3.0.0-1",
"fclone": "~1.0.11",
+ "nssocket": "0.6.0",
"pm2-axon": "~4.0.1",
"pm2-axon-rpc": "~0.7.0",
- "proxy-agent": "~6.4.0",
+ "proxy-agent": "~6.3.0",
"semver": "~7.5.0",
"ws": "~7.5.10"
}
@@ -1672,7 +1672,6 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
@@ -1686,14 +1685,12 @@
"version": "1.8.36",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz",
"integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==",
- "dev": true,
"license": "MIT"
},
"node_modules/@pm2/agent/node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -1711,7 +1708,6 @@
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
@@ -1724,7 +1720,6 @@
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
@@ -1740,7 +1735,6 @@
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -1753,27 +1747,12 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true,
"license": "ISC"
},
- "node_modules/@pm2/blessed": {
- "version": "0.1.81",
- "resolved": "https://registry.npmjs.org/@pm2/blessed/-/blessed-0.1.81.tgz",
- "integrity": "sha512-ZcNHqQjMuNRcQ7Z1zJbFIQZO/BDKV3KbiTckWdfbUaYhj7uNmUwb+FbdDWSCkvxNr9dBJQwvV17o6QBkAvgO0g==",
- "dev": true,
- "license": "MIT",
- "bin": {
- "blessed": "bin/tput.js"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
"node_modules/@pm2/io": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.1.0.tgz",
- "integrity": "sha512-IxHuYURa3+FQ6BKePlgChZkqABUKFYH6Bwbw7V/pWU1pP6iR1sCI26l7P9ThUEB385ruZn/tZS3CXDUF5IA1NQ==",
- "dev": true,
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz",
+ "integrity": "sha512-KiA+shC6sULQAr9mGZ1pg+6KVW9MF8NpG99x26Lf/082/Qy8qsTCtnJy+HQReW1A9Rdf0C/404cz0RZGZro+IA==",
"license": "Apache-2",
"dependencies": {
"async": "~2.6.1",
@@ -1793,7 +1772,6 @@
"version": "2.6.4",
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
- "dev": true,
"license": "MIT",
"dependencies": {
"lodash": "^4.17.14"
@@ -1803,7 +1781,6 @@
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
@@ -1821,14 +1798,12 @@
"version": "6.4.9",
"resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
"integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
- "dev": true,
"license": "MIT"
},
"node_modules/@pm2/io/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^4.0.0"
@@ -1841,7 +1816,6 @@
"version": "7.5.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
"integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "dev": true,
"license": "ISC",
"dependencies": {
"lru-cache": "^6.0.0"
@@ -1857,14 +1831,12 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
"integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
- "dev": true,
"license": "Apache-2.0"
},
"node_modules/@pm2/io/node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "dev": true,
"license": "ISC"
},
"node_modules/@pm2/js-api": {
@@ -2913,13 +2885,13 @@
]
},
"node_modules/accepts": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
- "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
"license": "MIT",
"dependencies": {
- "mime-types": "^3.0.0",
- "negotiator": "^1.0.0"
+ "mime-types": "~2.1.34",
+ "negotiator": "0.6.3"
},
"engines": {
"node": ">= 0.6"
@@ -2983,16 +2955,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
- "node_modules/ansis": {
- "version": "4.0.0-node10",
- "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.0.0-node10.tgz",
- "integrity": "sha512-BRrU0Bo1X9dFGw6KgGz6hWrqQuOlVEDOzkb0QSLZY9sXHqA7pNj7yHPVJRz7y/rj4EOJ3d/D5uxH+ee9leYgsg==",
- "dev": true,
- "license": "ISC",
- "engines": {
- "node": ">=10"
- }
- },
"node_modules/any-promise": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -3237,29 +3199,44 @@
"license": "MIT"
},
"node_modules/body-parser": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
- "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
+ "version": "1.20.4",
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
+ "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"license": "MIT",
"dependencies": {
- "bytes": "^3.1.2",
- "content-type": "^1.0.5",
- "debug": "^4.4.3",
- "http-errors": "^2.0.0",
- "iconv-lite": "^0.7.0",
- "on-finished": "^2.4.1",
- "qs": "^6.14.0",
- "raw-body": "^3.0.1",
- "type-is": "^2.0.1"
+ "bytes": "~3.1.2",
+ "content-type": "~1.0.5",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "~1.2.0",
+ "http-errors": "~2.0.1",
+ "iconv-lite": "~0.4.24",
+ "on-finished": "~2.4.1",
+ "qs": "~6.14.0",
+ "raw-body": "~2.5.3",
+ "type-is": "~1.6.18",
+ "unpipe": "~1.0.0"
},
"engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "node": ">= 0.8",
+ "npm": "1.2.8000 || >= 1.4.16"
+ }
+ },
+ "node_modules/body-parser/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
}
},
+ "node_modules/body-parser/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
@@ -3676,16 +3653,15 @@
}
},
"node_modules/content-disposition": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
- "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
+ "version": "0.5.4",
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
+ "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
"license": "MIT",
- "engines": {
- "node": ">=18"
+ "dependencies": {
+ "safe-buffer": "5.2.1"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "engines": {
+ "node": ">= 0.6"
}
},
"node_modules/content-type": {
@@ -3717,6 +3693,7 @@
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=6.6.0"
@@ -4088,49 +4065,6 @@
"node": ">=10.0.0"
}
},
- "node_modules/engine.io/node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/engine.io/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/engine.io/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/engine.io/node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/engine.io/node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
@@ -4384,48 +4318,72 @@
}
},
"node_modules/express": {
- "version": "5.2.1",
- "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
- "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
- "license": "MIT",
- "dependencies": {
- "accepts": "^2.0.0",
- "body-parser": "^2.2.1",
- "content-disposition": "^1.0.0",
- "content-type": "^1.0.5",
- "cookie": "^0.7.1",
- "cookie-signature": "^1.2.1",
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "finalhandler": "^2.1.0",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.0",
- "merge-descriptors": "^2.0.0",
- "mime-types": "^3.0.0",
- "on-finished": "^2.4.1",
- "once": "^1.4.0",
- "parseurl": "^1.3.3",
- "proxy-addr": "^2.0.7",
- "qs": "^6.14.0",
- "range-parser": "^1.2.1",
- "router": "^2.2.0",
- "send": "^1.1.0",
- "serve-static": "^2.2.0",
- "statuses": "^2.0.1",
- "type-is": "^2.0.1",
- "vary": "^1.1.2"
+ "version": "4.22.1",
+ "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
+ "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
+ "license": "MIT",
+ "dependencies": {
+ "accepts": "~1.3.8",
+ "array-flatten": "1.1.1",
+ "body-parser": "~1.20.3",
+ "content-disposition": "~0.5.4",
+ "content-type": "~1.0.4",
+ "cookie": "~0.7.1",
+ "cookie-signature": "~1.0.6",
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "finalhandler": "~1.3.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.0",
+ "merge-descriptors": "1.0.3",
+ "methods": "~1.1.2",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "path-to-regexp": "~0.1.12",
+ "proxy-addr": "~2.0.7",
+ "qs": "~6.14.0",
+ "range-parser": "~1.2.1",
+ "safe-buffer": "5.2.1",
+ "send": "~0.19.0",
+ "serve-static": "~1.16.2",
+ "setprototypeof": "1.2.0",
+ "statuses": "~2.0.1",
+ "type-is": "~1.6.18",
+ "utils-merge": "1.0.1",
+ "vary": "~1.1.2"
},
"engines": {
- "node": ">= 18"
+ "node": ">= 0.10.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
+ "node_modules/express/node_modules/cookie-signature": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
+ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
+ "license": "MIT"
+ },
+ "node_modules/express/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/express/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -4506,26 +4464,38 @@
}
},
"node_modules/finalhandler": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
- "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
+ "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
"license": "MIT",
"dependencies": {
- "debug": "^4.4.0",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "on-finished": "^2.4.1",
- "parseurl": "^1.3.3",
- "statuses": "^2.0.1"
+ "debug": "2.6.9",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "on-finished": "~2.4.1",
+ "parseurl": "~1.3.3",
+ "statuses": "~2.0.2",
+ "unpipe": "~1.0.0"
},
"engines": {
- "node": ">= 18.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/finalhandler/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
}
},
+ "node_modules/finalhandler/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
@@ -4562,27 +4532,6 @@
"node": ">= 6"
}
},
- "node_modules/form-data/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/form-data/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/formidable": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/formidable/-/formidable-3.5.4.tgz",
@@ -4625,12 +4574,12 @@
}
},
"node_modules/fresh": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
- "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
+ "version": "0.5.2",
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
+ "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
"license": "MIT",
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.6"
}
},
"node_modules/fsevents": {
@@ -4945,19 +4894,15 @@
}
},
"node_modules/iconv-lite": {
- "version": "0.7.1",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
- "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
+ "version": "0.4.24",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
+ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"license": "MIT",
"dependencies": {
- "safer-buffer": ">= 2.1.2 < 3.0.0"
+ "safer-buffer": ">= 2.1.2 < 3"
},
"engines": {
"node": ">=0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
}
},
"node_modules/ieee754": {
@@ -5145,12 +5090,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/is-promise": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
- "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
- "license": "MIT"
- },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -5627,22 +5566,19 @@
}
},
"node_modules/media-typer": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
- "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"license": "MIT",
"engines": {
- "node": ">= 0.8"
+ "node": ">= 0.6"
}
},
"node_modules/merge-descriptors": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
- "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
+ "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
"license": "MIT",
- "engines": {
- "node": ">=18"
- },
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
@@ -6151,28 +6087,24 @@
}
},
"node_modules/mime-db": {
- "version": "1.54.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
- "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
- "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
- "mime-db": "^1.54.0"
+ "mime-db": "1.52.0"
},
"engines": {
- "node": ">=18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "node": ">= 0.6"
}
},
"node_modules/mkdirp": {
@@ -6261,22 +6193,10 @@
"ms": "^2.1.1"
}
},
- "node_modules/needle/node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/negotiator": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
- "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
+ "version": "0.6.3",
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
@@ -6483,6 +6403,7 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"wrappy": "1"
@@ -6576,14 +6497,10 @@
"license": "MIT"
},
"node_modules/path-to-regexp": {
- "version": "8.3.0",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
- "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
- "license": "MIT",
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
+ "version": "0.1.12",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
+ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
+ "license": "MIT"
},
"node_modules/pathe": {
"version": "2.0.3",
@@ -6643,38 +6560,37 @@
}
},
"node_modules/pm2": {
- "version": "6.0.14",
- "resolved": "https://registry.npmjs.org/pm2/-/pm2-6.0.14.tgz",
- "integrity": "sha512-wX1FiFkzuT2H/UUEA8QNXDAA9MMHDsK/3UHj6Dkd5U7kxyigKDA5gyDw78ycTQZAuGCLWyUX5FiXEuVQWafukA==",
- "dev": true,
+ "version": "5.4.3",
+ "resolved": "https://registry.npmjs.org/pm2/-/pm2-5.4.3.tgz",
+ "integrity": "sha512-4/I1htIHzZk1Y67UgOCo4F1cJtas1kSds31N8zN0PybO230id1nigyjGuGFzUnGmUFPmrJ0On22fO1ChFlp7VQ==",
"license": "AGPL-3.0",
"dependencies": {
- "@pm2/agent": "~2.1.1",
- "@pm2/blessed": "0.1.81",
- "@pm2/io": "~6.1.0",
+ "@pm2/agent": "~2.0.0",
+ "@pm2/io": "~6.0.1",
"@pm2/js-api": "~0.8.0",
- "@pm2/pm2-version-check": "^1.0.4",
- "ansis": "4.0.0-node10",
- "async": "3.2.6",
- "chokidar": "3.6.0",
- "cli-tableau": "2.0.1",
+ "@pm2/pm2-version-check": "latest",
+ "async": "~3.2.0",
+ "blessed": "0.1.81",
+ "chalk": "3.0.0",
+ "chokidar": "^3.5.3",
+ "cli-tableau": "^2.0.0",
"commander": "2.15.1",
- "croner": "4.1.97",
- "dayjs": "1.11.15",
- "debug": "4.4.3",
+ "croner": "~4.1.92",
+ "dayjs": "~1.11.5",
+ "debug": "^4.3.1",
"enquirer": "2.3.6",
"eventemitter2": "5.0.1",
"fclone": "1.0.11",
- "js-yaml": "4.1.1",
+ "js-yaml": "~4.1.0",
"mkdirp": "1.0.4",
"needle": "2.4.0",
- "pidusage": "3.0.2",
+ "pidusage": "~3.0",
"pm2-axon": "~4.0.1",
"pm2-axon-rpc": "~0.7.1",
"pm2-deploy": "~1.0.2",
"pm2-multimeter": "^0.1.2",
- "promptly": "2.2.0",
- "semver": "7.7.2",
+ "promptly": "^2",
+ "semver": "^7.2",
"source-map-support": "0.5.21",
"sprintf-js": "1.1.2",
"vizion": "~2.2.1"
@@ -6686,7 +6602,7 @@
"pm2-runtime": "bin/pm2-runtime"
},
"engines": {
- "node": ">=16.0.0"
+ "node": ">=12.0.0"
},
"optionalDependencies": {
"pm2-sysmonit": "^1.2.8"
@@ -6768,11 +6684,23 @@
"node": ">=8"
}
},
+ "node_modules/pm2/node_modules/chalk": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
+ "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/pm2/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
- "dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -6781,10 +6709,22 @@
"node": ">=10"
}
},
+ "node_modules/pm2/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/portos-ai-toolkit": {
- "version": "0.4.0",
- "resolved": "git+ssh://git@github.com/atomantic/portos-ai-toolkit.git#ac13168156f81c90dab57702c0de3f97de7970a9",
- "integrity": "sha512-5qzNAhCCCSCAelHhOTRyjYBRmSYojO4p2Tqf6Jy4gn6iBnKIiuuCW2EBIM4MbpfmbgEXdwt1ll4EKu4kRnmRlQ==",
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/portos-ai-toolkit/-/portos-ai-toolkit-0.5.0.tgz",
+ "integrity": "sha512-2ZJjQYa0CDT0yDapozWHsVpz2nIeqBlXKQeaFOxlqFCHEOryqs96Qj2YyD2XZwH6kN4Il1vrbv58R6SA6H6W1A==",
"license": "MIT",
"dependencies": {
"uuid": "^11.0.3",
@@ -7052,16 +6992,15 @@
}
},
"node_modules/proxy-agent": {
- "version": "6.4.0",
- "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.4.0.tgz",
- "integrity": "sha512-u0piLU+nCOHMgGjRbimiXmA9kM/L9EHh3zL81xCdp7m+Y2pHIsnmbdDoEDoAz5geaonNR6q6+yOPQs6n4T6sBQ==",
- "dev": true,
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz",
+ "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.0.2",
"debug": "^4.3.4",
- "http-proxy-agent": "^7.0.1",
- "https-proxy-agent": "^7.0.3",
+ "http-proxy-agent": "^7.0.0",
+ "https-proxy-agent": "^7.0.2",
"lru-cache": "^7.14.1",
"pac-proxy-agent": "^7.0.1",
"proxy-from-env": "^1.1.0",
@@ -7075,7 +7014,6 @@
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
- "dev": true,
"license": "ISC",
"engines": {
"node": ">=12"
@@ -7133,18 +7071,18 @@
}
},
"node_modules/raw-body": {
- "version": "3.0.2",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
- "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
+ "version": "2.5.3",
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
+ "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
- "iconv-lite": "~0.7.0",
+ "iconv-lite": "~0.4.24",
"unpipe": "~1.0.0"
},
"engines": {
- "node": ">= 0.10"
+ "node": ">= 0.8"
}
},
"node_modules/react": {
@@ -7511,22 +7449,6 @@
"fsevents": "~2.3.2"
}
},
- "node_modules/router": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
- "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
- "license": "MIT",
- "dependencies": {
- "debug": "^4.4.0",
- "depd": "^2.0.0",
- "is-promise": "^4.0.0",
- "parseurl": "^1.3.3",
- "path-to-regexp": "^8.0.0"
- },
- "engines": {
- "node": ">= 18"
- }
- },
"node_modules/run-parallel": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -7636,48 +7558,69 @@
}
},
"node_modules/send": {
- "version": "1.2.1",
- "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
- "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
+ "version": "0.19.2",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
+ "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
"license": "MIT",
"dependencies": {
- "debug": "^4.4.3",
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "etag": "^1.8.1",
- "fresh": "^2.0.0",
- "http-errors": "^2.0.1",
- "mime-types": "^3.0.2",
- "ms": "^2.1.3",
- "on-finished": "^2.4.1",
- "range-parser": "^1.2.1",
- "statuses": "^2.0.2"
+ "debug": "2.6.9",
+ "depd": "2.0.0",
+ "destroy": "1.2.0",
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "etag": "~1.8.1",
+ "fresh": "~0.5.2",
+ "http-errors": "~2.0.1",
+ "mime": "1.6.0",
+ "ms": "2.1.3",
+ "on-finished": "~2.4.1",
+ "range-parser": "~1.2.1",
+ "statuses": "~2.0.2"
},
"engines": {
- "node": ">= 18"
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/send/node_modules/debug": {
+ "version": "2.6.9",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
+ "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "2.0.0"
+ }
+ },
+ "node_modules/send/node_modules/debug/node_modules/ms": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
+ "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
+ "license": "MIT"
+ },
+ "node_modules/send/node_modules/mime": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
+ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
+ "license": "MIT",
+ "bin": {
+ "mime": "cli.js"
},
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "engines": {
+ "node": ">=4"
}
},
"node_modules/serve-static": {
- "version": "2.2.1",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
- "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
+ "version": "1.16.3",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
+ "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
"license": "MIT",
"dependencies": {
- "encodeurl": "^2.0.0",
- "escape-html": "^1.0.3",
- "parseurl": "^1.3.3",
- "send": "^1.2.0"
+ "encodeurl": "~2.0.0",
+ "escape-html": "~1.0.3",
+ "parseurl": "~1.3.3",
+ "send": "~0.19.1"
},
"engines": {
- "node": ">= 18"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
+ "node": ">= 0.8.0"
}
},
"node_modules/set-cookie-parser": {
@@ -7964,49 +7907,6 @@
"node": ">=10.0.0"
}
},
- "node_modules/socket.io/node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/socket.io/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/socket.io/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "node_modules/socket.io/node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
"node_modules/socks": {
"version": "2.8.7",
"resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz",
@@ -8699,14 +8599,13 @@
}
},
"node_modules/type-is": {
- "version": "2.0.1",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
- "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
+ "version": "1.6.18",
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"license": "MIT",
"dependencies": {
- "content-type": "^1.0.5",
- "media-typer": "^1.1.0",
- "mime-types": "^3.0.0"
+ "media-typer": "0.3.0",
+ "mime-types": "~2.1.24"
},
"engines": {
"node": ">= 0.6"
@@ -9215,6 +9114,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+ "dev": true,
"license": "ISC"
},
"node_modules/ws": {
@@ -9368,7 +9268,7 @@
},
"server": {
"name": "portos-server",
- "version": "0.14.21",
+ "version": "0.15.15",
"dependencies": {
"axios": "^1.7.9",
"cors": "^2.8.5",
@@ -9388,652 +9288,6 @@
"vitest": "^4.0.16"
}
},
- "server/node_modules/@pm2/agent": {
- "version": "2.0.4",
- "resolved": "https://registry.npmjs.org/@pm2/agent/-/agent-2.0.4.tgz",
- "integrity": "sha512-n7WYvvTJhHLS2oBb1PjOtgLpMhgImOq8sXkPBw6smeg9LJBWZjiEgPKOpR8mn9UJZsB5P3W4V/MyvNnp31LKeA==",
- "license": "AGPL-3.0",
- "dependencies": {
- "async": "~3.2.0",
- "chalk": "~3.0.0",
- "dayjs": "~1.8.24",
- "debug": "~4.3.1",
- "eventemitter2": "~5.0.1",
- "fast-json-patch": "^3.0.0-1",
- "fclone": "~1.0.11",
- "nssocket": "0.6.0",
- "pm2-axon": "~4.0.1",
- "pm2-axon-rpc": "~0.7.0",
- "proxy-agent": "~6.3.0",
- "semver": "~7.5.0",
- "ws": "~7.5.10"
- }
- },
- "server/node_modules/@pm2/agent/node_modules/dayjs": {
- "version": "1.8.36",
- "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.8.36.tgz",
- "integrity": "sha512-3VmRXEtw7RZKAf+4Tv1Ym9AGeo8r8+CjDi26x+7SYQil1UqtqdaokhzoEJohqlzt0m5kacJSDhJQkG/LWhpRBw==",
- "license": "MIT"
- },
- "server/node_modules/@pm2/agent/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "server/node_modules/@pm2/agent/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "server/node_modules/@pm2/agent/node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "license": "ISC",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "server/node_modules/@pm2/agent/node_modules/ws": {
- "version": "7.5.10",
- "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
- "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8.3.0"
- },
- "peerDependencies": {
- "bufferutil": "^4.0.1",
- "utf-8-validate": "^5.0.2"
- },
- "peerDependenciesMeta": {
- "bufferutil": {
- "optional": true
- },
- "utf-8-validate": {
- "optional": true
- }
- }
- },
- "server/node_modules/@pm2/io": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/@pm2/io/-/io-6.0.1.tgz",
- "integrity": "sha512-KiA+shC6sULQAr9mGZ1pg+6KVW9MF8NpG99x26Lf/082/Qy8qsTCtnJy+HQReW1A9Rdf0C/404cz0RZGZro+IA==",
- "license": "Apache-2",
- "dependencies": {
- "async": "~2.6.1",
- "debug": "~4.3.1",
- "eventemitter2": "^6.3.1",
- "require-in-the-middle": "^5.0.0",
- "semver": "~7.5.4",
- "shimmer": "^1.2.0",
- "signal-exit": "^3.0.3",
- "tslib": "1.9.3"
- },
- "engines": {
- "node": ">=6.0"
- }
- },
- "server/node_modules/@pm2/io/node_modules/async": {
- "version": "2.6.4",
- "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
- "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
- "license": "MIT",
- "dependencies": {
- "lodash": "^4.17.14"
- }
- },
- "server/node_modules/@pm2/io/node_modules/debug": {
- "version": "4.3.7",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
- "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "server/node_modules/@pm2/io/node_modules/eventemitter2": {
- "version": "6.4.9",
- "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz",
- "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==",
- "license": "MIT"
- },
- "server/node_modules/@pm2/io/node_modules/lru-cache": {
- "version": "6.0.0",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
- "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
- "license": "ISC",
- "dependencies": {
- "yallist": "^4.0.0"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "server/node_modules/@pm2/io/node_modules/semver": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz",
- "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==",
- "license": "ISC",
- "dependencies": {
- "lru-cache": "^6.0.0"
- },
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "server/node_modules/accepts": {
- "version": "1.3.8",
- "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
- "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
- "license": "MIT",
- "dependencies": {
- "mime-types": "~2.1.34",
- "negotiator": "0.6.3"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "server/node_modules/body-parser": {
- "version": "1.20.4",
- "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
- "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "content-type": "~1.0.5",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "~1.2.0",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.4.24",
- "on-finished": "~2.4.1",
- "qs": "~6.14.0",
- "raw-body": "~2.5.3",
- "type-is": "~1.6.18",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8",
- "npm": "1.2.8000 || >= 1.4.16"
- }
- },
- "server/node_modules/chalk": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
- "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.1.0",
- "supports-color": "^7.1.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "server/node_modules/content-disposition": {
- "version": "0.5.4",
- "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
- "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
- "license": "MIT",
- "dependencies": {
- "safe-buffer": "5.2.1"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "server/node_modules/cookie-signature": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
- "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
- "license": "MIT"
- },
- "server/node_modules/debug": {
- "version": "2.6.9",
- "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
- "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
- "license": "MIT",
- "dependencies": {
- "ms": "2.0.0"
- }
- },
- "server/node_modules/debug/node_modules/ms": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
- "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
- "license": "MIT"
- },
- "server/node_modules/express": {
- "version": "4.22.1",
- "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
- "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
- "license": "MIT",
- "dependencies": {
- "accepts": "~1.3.8",
- "array-flatten": "1.1.1",
- "body-parser": "~1.20.3",
- "content-disposition": "~0.5.4",
- "content-type": "~1.0.4",
- "cookie": "~0.7.1",
- "cookie-signature": "~1.0.6",
- "debug": "2.6.9",
- "depd": "2.0.0",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "finalhandler": "~1.3.1",
- "fresh": "~0.5.2",
- "http-errors": "~2.0.0",
- "merge-descriptors": "1.0.3",
- "methods": "~1.1.2",
- "on-finished": "~2.4.1",
- "parseurl": "~1.3.3",
- "path-to-regexp": "~0.1.12",
- "proxy-addr": "~2.0.7",
- "qs": "~6.14.0",
- "range-parser": "~1.2.1",
- "safe-buffer": "5.2.1",
- "send": "~0.19.0",
- "serve-static": "~1.16.2",
- "setprototypeof": "1.2.0",
- "statuses": "~2.0.1",
- "type-is": "~1.6.18",
- "utils-merge": "1.0.1",
- "vary": "~1.1.2"
- },
- "engines": {
- "node": ">= 0.10.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/express"
- }
- },
- "server/node_modules/finalhandler": {
- "version": "1.3.2",
- "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
- "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "on-finished": "~2.4.1",
- "parseurl": "~1.3.3",
- "statuses": "~2.0.2",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "server/node_modules/fresh": {
- "version": "0.5.2",
- "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
- "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "server/node_modules/iconv-lite": {
- "version": "0.4.24",
- "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
- "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
- "license": "MIT",
- "dependencies": {
- "safer-buffer": ">= 2.1.2 < 3"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "server/node_modules/lru-cache": {
- "version": "7.18.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
- "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
- "license": "ISC",
- "engines": {
- "node": ">=12"
- }
- },
- "server/node_modules/media-typer": {
- "version": "0.3.0",
- "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
- "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "server/node_modules/merge-descriptors": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
- "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
- "license": "MIT",
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "server/node_modules/mime": {
- "version": "1.6.0",
- "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
- "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
- "license": "MIT",
- "bin": {
- "mime": "cli.js"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "server/node_modules/mime-db": {
- "version": "1.52.0",
- "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
- "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "server/node_modules/mime-types": {
- "version": "2.1.35",
- "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
- "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
- "license": "MIT",
- "dependencies": {
- "mime-db": "1.52.0"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
- "server/node_modules/negotiator": {
- "version": "0.6.3",
- "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
- "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
- "license": "MIT",
- "engines": {
- "node": ">= 0.6"
- }
- },
- "server/node_modules/path-to-regexp": {
- "version": "0.1.12",
- "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
- "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
- "license": "MIT"
- },
- "server/node_modules/pm2": {
- "version": "5.4.3",
- "resolved": "https://registry.npmjs.org/pm2/-/pm2-5.4.3.tgz",
- "integrity": "sha512-4/I1htIHzZk1Y67UgOCo4F1cJtas1kSds31N8zN0PybO230id1nigyjGuGFzUnGmUFPmrJ0On22fO1ChFlp7VQ==",
- "license": "AGPL-3.0",
- "dependencies": {
- "@pm2/agent": "~2.0.0",
- "@pm2/io": "~6.0.1",
- "@pm2/js-api": "~0.8.0",
- "@pm2/pm2-version-check": "latest",
- "async": "~3.2.0",
- "blessed": "0.1.81",
- "chalk": "3.0.0",
- "chokidar": "^3.5.3",
- "cli-tableau": "^2.0.0",
- "commander": "2.15.1",
- "croner": "~4.1.92",
- "dayjs": "~1.11.5",
- "debug": "^4.3.1",
- "enquirer": "2.3.6",
- "eventemitter2": "5.0.1",
- "fclone": "1.0.11",
- "js-yaml": "~4.1.0",
- "mkdirp": "1.0.4",
- "needle": "2.4.0",
- "pidusage": "~3.0",
- "pm2-axon": "~4.0.1",
- "pm2-axon-rpc": "~0.7.1",
- "pm2-deploy": "~1.0.2",
- "pm2-multimeter": "^0.1.2",
- "promptly": "^2",
- "semver": "^7.2",
- "source-map-support": "0.5.21",
- "sprintf-js": "1.1.2",
- "vizion": "~2.2.1"
- },
- "bin": {
- "pm2": "bin/pm2",
- "pm2-dev": "bin/pm2-dev",
- "pm2-docker": "bin/pm2-docker",
- "pm2-runtime": "bin/pm2-runtime"
- },
- "engines": {
- "node": ">=12.0.0"
- },
- "optionalDependencies": {
- "pm2-sysmonit": "^1.2.8"
- }
- },
- "server/node_modules/pm2/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "server/node_modules/portos-ai-toolkit": {
- "version": "0.5.0",
- "resolved": "https://registry.npmjs.org/portos-ai-toolkit/-/portos-ai-toolkit-0.5.0.tgz",
- "integrity": "sha512-2ZJjQYa0CDT0yDapozWHsVpz2nIeqBlXKQeaFOxlqFCHEOryqs96Qj2YyD2XZwH6kN4Il1vrbv58R6SA6H6W1A==",
- "license": "MIT",
- "dependencies": {
- "uuid": "^11.0.3",
- "zod": "^3.24.1"
- },
- "peerDependencies": {
- "express": "^4.21.2 || ^5.2.1",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
- "socket.io": "^4.8.3",
- "socket.io-client": "^4.8.3"
- },
- "peerDependenciesMeta": {
- "express": {
- "optional": true
- },
- "react": {
- "optional": true
- },
- "react-dom": {
- "optional": true
- },
- "socket.io": {
- "optional": true
- },
- "socket.io-client": {
- "optional": true
- }
- }
- },
- "server/node_modules/proxy-agent": {
- "version": "6.3.1",
- "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.3.1.tgz",
- "integrity": "sha512-Rb5RVBy1iyqOtNl15Cw/llpeLH8bsb37gM1FUfKQ+Wck6xHlbAhWGUFiTRHtkjqGTA5pSHz6+0hrPW/oECihPQ==",
- "license": "MIT",
- "dependencies": {
- "agent-base": "^7.0.2",
- "debug": "^4.3.4",
- "http-proxy-agent": "^7.0.0",
- "https-proxy-agent": "^7.0.2",
- "lru-cache": "^7.14.1",
- "pac-proxy-agent": "^7.0.1",
- "proxy-from-env": "^1.1.0",
- "socks-proxy-agent": "^8.0.2"
- },
- "engines": {
- "node": ">= 14"
- }
- },
- "server/node_modules/proxy-agent/node_modules/debug": {
- "version": "4.4.3",
- "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
- "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
- "license": "MIT",
- "dependencies": {
- "ms": "^2.1.3"
- },
- "engines": {
- "node": ">=6.0"
- },
- "peerDependenciesMeta": {
- "supports-color": {
- "optional": true
- }
- }
- },
- "server/node_modules/raw-body": {
- "version": "2.5.3",
- "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
- "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
- "license": "MIT",
- "dependencies": {
- "bytes": "~3.1.2",
- "http-errors": "~2.0.1",
- "iconv-lite": "~0.4.24",
- "unpipe": "~1.0.0"
- },
- "engines": {
- "node": ">= 0.8"
- }
- },
- "server/node_modules/semver": {
- "version": "7.7.3",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
- "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
- "license": "ISC",
- "bin": {
- "semver": "bin/semver.js"
- },
- "engines": {
- "node": ">=10"
- }
- },
- "server/node_modules/send": {
- "version": "0.19.2",
- "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
- "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
- "license": "MIT",
- "dependencies": {
- "debug": "2.6.9",
- "depd": "2.0.0",
- "destroy": "1.2.0",
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "etag": "~1.8.1",
- "fresh": "~0.5.2",
- "http-errors": "~2.0.1",
- "mime": "1.6.0",
- "ms": "2.1.3",
- "on-finished": "~2.4.1",
- "range-parser": "~1.2.1",
- "statuses": "~2.0.2"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "server/node_modules/serve-static": {
- "version": "1.16.3",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
- "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
- "license": "MIT",
- "dependencies": {
- "encodeurl": "~2.0.0",
- "escape-html": "~1.0.3",
- "parseurl": "~1.3.3",
- "send": "~0.19.1"
- },
- "engines": {
- "node": ">= 0.8.0"
- }
- },
- "server/node_modules/supports-color": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
- "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
- "license": "MIT",
- "dependencies": {
- "has-flag": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "server/node_modules/tslib": {
- "version": "1.9.3",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.9.3.tgz",
- "integrity": "sha512-4krF8scpejhaOgqzBEcGM7yDIEfi0/8+8zDRZhNZZ2kjmHJ4hv3zCbQWxoJGz1iw5U0Jl0nma13xzHXcncMavQ==",
- "license": "Apache-2.0"
- },
- "server/node_modules/type-is": {
- "version": "1.6.18",
- "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
- "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
- "license": "MIT",
- "dependencies": {
- "media-typer": "0.3.0",
- "mime-types": "~2.1.24"
- },
- "engines": {
- "node": ">= 0.6"
- }
- },
"server/node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
@@ -10054,12 +9308,6 @@
"optional": true
}
}
- },
- "server/node_modules/yallist": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
- "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
- "license": "ISC"
}
}
}
diff --git a/package.json b/package.json
index 4dd8f900..90d75bd8 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "portos",
- "version": "0.14.21",
+ "version": "0.15.15",
"private": true,
"description": "Local dev machine App OS portal",
"author": "Adam Eivy (@antic|@atomantic)",
@@ -22,7 +22,6 @@
"pm2:restart": "pm2 restart ecosystem.config.cjs",
"pm2:logs": "pm2 logs",
"pm2:status": "pm2 status",
- "pm2:kill": "pm2 kill",
"install:all": "npm install && cd client && npm install && cd ../server && npm install && cd .. && npm run setup",
"setup": "node scripts/setup-data.js && node scripts/setup-browser.js",
"setup:data": "node scripts/setup-data.js",
@@ -35,13 +34,13 @@
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"concurrently": "^8.2.2",
- "pm2": "^6.0.14",
+ "pm2": "^5.4.3",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"vite": "^6.0.6"
},
"dependencies": {
- "express": "^5.2.1",
- "portos-ai-toolkit": "github:atomantic/portos-ai-toolkit#ac13168156f81c90dab57702c0de3f97de7970a9"
+ "express": "^4.21.2",
+ "portos-ai-toolkit": "^0.5.0"
}
}
diff --git a/server/package.json b/server/package.json
index 359755ef..70922631 100644
--- a/server/package.json
+++ b/server/package.json
@@ -1,6 +1,6 @@
{
"name": "portos-server",
- "version": "0.14.21",
+ "version": "0.15.15",
"private": true,
"type": "module",
"scripts": {
diff --git a/server/routes/agentTools.js b/server/routes/agentTools.js
index b4bbaa33..d72f8698 100644
--- a/server/routes/agentTools.js
+++ b/server/routes/agentTools.js
@@ -77,7 +77,7 @@ router.post('/generate-comment', asyncHandler(async (req, res) => {
const post = await client.getPost(data.postId);
const commentsResponse = await client.getComments(data.postId);
- const comments = commentsResponse.comments || commentsResponse || [];
+ const comments = commentsResponse?.comments ?? commentsResponse ?? [];
let generated;
if (data.parentId) {
diff --git a/server/routes/apps.js b/server/routes/apps.js
index 33a884c7..d3a91868 100644
--- a/server/routes/apps.js
+++ b/server/routes/apps.js
@@ -11,6 +11,19 @@ import { parseEcosystemFromPath } from '../services/streamingDetect.js';
const router = Router();
+/**
+ * Middleware to load app by :id param and attach to req.loadedApp
+ * Throws 404 if not found, eliminating repeated null checks across routes
+ */
+const loadApp = asyncHandler(async (req, res, next) => {
+ const app = await appsService.getAppById(req.params.id);
+ if (!app) {
+ throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
+ }
+ req.loadedApp = app;
+ next();
+});
+
// GET /api/apps - List all apps
router.get('/', asyncHandler(async (req, res) => {
const apps = await appsService.getAllApps();
@@ -40,7 +53,7 @@ router.get('/', asyncHandler(async (req, res) => {
const statuses = {};
for (const processName of app.pm2ProcessNames || []) {
const pm2Proc = pm2Map.get(processName);
- statuses[processName] = pm2Proc || { name: processName, status: 'not_found', pm2_env: null };
+ statuses[processName] = pm2Proc ?? { name: processName, status: 'not_found', pm2_env: null };
}
// Compute overall status
@@ -73,12 +86,8 @@ router.get('/', asyncHandler(async (req, res) => {
}));
// GET /api/apps/:id - Get single app
-router.get('/:id', asyncHandler(async (req, res, next) => {
- const app = await appsService.getAppById(req.params.id);
-
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
+router.get('/:id', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
// Get PM2 status for each process (using app's custom PM2_HOME if set)
const statuses = {};
@@ -165,11 +174,8 @@ router.post('/:id/unarchive', asyncHandler(async (req, res) => {
}));
// GET /api/apps/:id/task-types - Get per-app task type overrides
-router.get('/:id/task-types', asyncHandler(async (req, res) => {
- const app = await appsService.getAppById(req.params.id);
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
+router.get('/:id/task-types', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
res.json({ appId: app.id, appName: app.name, disabledTaskTypes: app.disabledTaskTypes || [] });
}));
@@ -190,12 +196,8 @@ router.put('/:id/task-types/:taskType', asyncHandler(async (req, res) => {
}));
// POST /api/apps/:id/start - Start app via PM2
-router.post('/:id/start', asyncHandler(async (req, res, next) => {
- const app = await appsService.getAppById(req.params.id);
-
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
+router.post('/:id/start', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
const processNames = app.pm2ProcessNames || [app.name.toLowerCase().replace(/\s+/g, '-')];
@@ -234,13 +236,8 @@ router.post('/:id/start', asyncHandler(async (req, res, next) => {
}));
// POST /api/apps/:id/stop - Stop app
-router.post('/:id/stop', asyncHandler(async (req, res, next) => {
- const app = await appsService.getAppById(req.params.id);
-
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
-
+router.post('/:id/stop', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
const results = {};
for (const name of app.pm2ProcessNames || []) {
@@ -257,13 +254,8 @@ router.post('/:id/stop', asyncHandler(async (req, res, next) => {
}));
// POST /api/apps/:id/restart - Restart app
-router.post('/:id/restart', asyncHandler(async (req, res, next) => {
- const app = await appsService.getAppById(req.params.id);
-
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
-
+router.post('/:id/restart', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
const results = {};
for (const name of app.pm2ProcessNames || []) {
@@ -280,13 +272,8 @@ router.post('/:id/restart', asyncHandler(async (req, res, next) => {
}));
// GET /api/apps/:id/status - Get PM2 status
-router.get('/:id/status', asyncHandler(async (req, res, next) => {
- const app = await appsService.getAppById(req.params.id);
-
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
-
+router.get('/:id/status', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
const statuses = {};
for (const name of app.pm2ProcessNames || []) {
@@ -299,13 +286,8 @@ router.get('/:id/status', asyncHandler(async (req, res, next) => {
}));
// GET /api/apps/:id/logs - Get logs
-router.get('/:id/logs', asyncHandler(async (req, res, next) => {
- const app = await appsService.getAppById(req.params.id);
-
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
-
+router.get('/:id/logs', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
const lines = parseInt(req.query.lines) || 100;
const processName = req.query.process || app.pm2ProcessNames?.[0];
@@ -343,12 +325,8 @@ const ALLOWED_EDITORS = new Set([
]);
// POST /api/apps/:id/open-editor - Open app in editor
-router.post('/:id/open-editor', asyncHandler(async (req, res, next) => {
- const app = await appsService.getAppById(req.params.id);
-
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
+router.post('/:id/open-editor', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
if (!existsSync(app.repoPath)) {
throw new ServerError('App path does not exist', { status: 400, code: 'PATH_NOT_FOUND' });
@@ -391,12 +369,8 @@ router.post('/:id/open-editor', asyncHandler(async (req, res, next) => {
}));
// POST /api/apps/:id/open-folder - Open app folder in file manager
-router.post('/:id/open-folder', asyncHandler(async (req, res, next) => {
- const app = await appsService.getAppById(req.params.id);
-
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
+router.post('/:id/open-folder', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
if (!existsSync(app.repoPath)) {
throw new ServerError('App path does not exist', { status: 400, code: 'PATH_NOT_FOUND' });
@@ -427,12 +401,8 @@ router.post('/:id/open-folder', asyncHandler(async (req, res, next) => {
}));
// POST /api/apps/:id/refresh-config - Re-parse ecosystem config for PM2 processes
-router.post('/:id/refresh-config', asyncHandler(async (req, res, next) => {
- const app = await appsService.getAppById(req.params.id);
-
- if (!app) {
- throw new ServerError('App not found', { status: 404, code: 'NOT_FOUND' });
- }
+router.post('/:id/refresh-config', loadApp, asyncHandler(async (req, res) => {
+ const app = req.loadedApp;
if (!existsSync(app.repoPath)) {
throw new ServerError('App path does not exist', { status: 400, code: 'PATH_NOT_FOUND' });
diff --git a/server/routes/brain.js b/server/routes/brain.js
index 3150b8ef..aefd4211 100644
--- a/server/routes/brain.js
+++ b/server/routes/brain.js
@@ -642,7 +642,9 @@ router.post('/links', asyncHandler(async (req, res) => {
// If GitHub repo and auto-clone enabled, start clone in background
if (isGitHubRepo && autoClone !== false) {
- cloneRepoInBackground(link.id, url);
+ cloneRepoInBackground(link.id, url).catch(err => {
+ console.error(`β Background clone setup failed for ${link.id}: ${err.message}`);
+ });
}
res.status(201).json(link);
diff --git a/server/routes/cos.js b/server/routes/cos.js
index 146a4947..87c4fc39 100644
--- a/server/routes/cos.js
+++ b/server/routes/cos.js
@@ -19,6 +19,19 @@ import { asyncHandler, ServerError } from '../lib/errorHandler.js';
const router = Router();
+const SCHEDULE_FIELDS = ['type', 'enabled', 'intervalMs', 'providerId', 'model', 'prompt'];
+
+/**
+ * Pick only defined values from body for schedule settings updates
+ */
+function pickScheduleSettings(body) {
+ const settings = {};
+ for (const key of SCHEDULE_FIELDS) {
+ if (body[key] !== undefined) settings[key] = body[key];
+ }
+ return settings;
+}
+
// GET /api/cos - Get CoS status
router.get('/', asyncHandler(async (req, res) => {
const status = await cos.getStatus();
@@ -586,17 +599,7 @@ router.get('/schedule/self-improvement/:taskType', asyncHandler(async (req, res)
// PUT /api/cos/schedule/self-improvement/:taskType - Update interval for self-improvement task
router.put('/schedule/self-improvement/:taskType', asyncHandler(async (req, res) => {
const { taskType } = req.params;
- const { type, enabled, intervalMs, providerId, model, prompt } = req.body;
-
- const settings = {};
- if (type !== undefined) settings.type = type;
- if (enabled !== undefined) settings.enabled = enabled;
- if (intervalMs !== undefined) settings.intervalMs = intervalMs;
- if (providerId !== undefined) settings.providerId = providerId;
- if (model !== undefined) settings.model = model;
- if (prompt !== undefined) settings.prompt = prompt;
-
- const result = await taskSchedule.updateSelfImprovementInterval(taskType, settings);
+ const result = await taskSchedule.updateSelfImprovementInterval(taskType, pickScheduleSettings(req.body));
res.json({ success: true, taskType, interval: result });
}));
@@ -610,17 +613,7 @@ router.get('/schedule/app-improvement/:taskType', asyncHandler(async (req, res)
// PUT /api/cos/schedule/app-improvement/:taskType - Update interval for app improvement task
router.put('/schedule/app-improvement/:taskType', asyncHandler(async (req, res) => {
const { taskType } = req.params;
- const { type, enabled, intervalMs, providerId, model, prompt } = req.body;
-
- const settings = {};
- if (type !== undefined) settings.type = type;
- if (enabled !== undefined) settings.enabled = enabled;
- if (intervalMs !== undefined) settings.intervalMs = intervalMs;
- if (providerId !== undefined) settings.providerId = providerId;
- if (model !== undefined) settings.model = model;
- if (prompt !== undefined) settings.prompt = prompt;
-
- const result = await taskSchedule.updateAppImprovementInterval(taskType, settings);
+ const result = await taskSchedule.updateAppImprovementInterval(taskType, pickScheduleSettings(req.body));
res.json({ success: true, taskType, interval: result });
}));
@@ -937,9 +930,9 @@ router.get('/productivity/calendar', asyncHandler(async (req, res) => {
// Surfaces the most important things to address right now across all CoS subsystems
router.get('/actionable-insights', asyncHandler(async (req, res) => {
const [tasksData, learningSummary, healthCheck, notificationsModule] = await Promise.all([
- cos.getAllTasks().catch(() => ({ user: null, cos: null })),
- taskLearning.getLearningInsights().catch(() => null),
- cos.runHealthCheck().catch(() => ({ issues: [] })),
+ cos.getAllTasks().catch(err => { console.error(`β Failed to load tasks: ${err.message}`); return { user: null, cos: null }; }),
+ taskLearning.getLearningInsights().catch(err => { console.error(`β Failed to load learning insights: ${err.message}`); return null; }),
+ cos.runHealthCheck().catch(err => { console.error(`β Failed to run health check: ${err.message}`); return { issues: [] }; }),
import('../services/notifications.js')
]);
diff --git a/server/routes/prompts.old.js b/server/routes/prompts.old.js
deleted file mode 100644
index 9c96ebbb..00000000
--- a/server/routes/prompts.old.js
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Router } from 'express';
-import * as promptService from '../services/promptService.js';
-import { asyncHandler, ServerError } from '../lib/errorHandler.js';
-
-const router = Router();
-
-// GET /api/prompts - List all stages
-router.get('/', asyncHandler(async (req, res) => {
- const stages = promptService.getStages();
- res.json({ stages });
-}));
-
-// GET /api/prompts/variables - List all variables
-router.get('/variables', asyncHandler(async (req, res) => {
- const variables = promptService.getVariables();
- res.json({ variables });
-}));
-
-// GET /api/prompts/variables/:key - Get a variable
-router.get('/variables/:key', asyncHandler(async (req, res) => {
- const variable = promptService.getVariable(req.params.key);
- if (!variable) {
- throw new ServerError('Variable not found', { status: 404, code: 'NOT_FOUND' });
- }
- res.json({ key: req.params.key, ...variable });
-}));
-
-// POST /api/prompts/variables - Create a variable
-router.post('/variables', asyncHandler(async (req, res) => {
- const { key, name, category, content } = req.body;
- if (!key || !content) {
- throw new ServerError('key and content are required', { status: 400, code: 'VALIDATION_ERROR' });
- }
- await promptService.createVariable(key, { name, category, content });
- res.json({ success: true, key });
-}));
-
-// PUT /api/prompts/variables/:key - Update a variable
-router.put('/variables/:key', asyncHandler(async (req, res) => {
- const { name, category, content } = req.body;
- await promptService.updateVariable(req.params.key, { name, category, content });
- res.json({ success: true });
-}));
-
-// DELETE /api/prompts/variables/:key - Delete a variable
-router.delete('/variables/:key', asyncHandler(async (req, res) => {
- await promptService.deleteVariable(req.params.key);
- res.json({ success: true });
-}));
-
-// GET /api/prompts/:stage - Get stage with template
-router.get('/:stage', asyncHandler(async (req, res) => {
- const stage = promptService.getStage(req.params.stage);
- if (!stage) {
- throw new ServerError('Stage not found', { status: 404, code: 'NOT_FOUND' });
- }
- const template = await promptService.getStageTemplate(req.params.stage);
- res.json({ ...stage, template });
-}));
-
-// PUT /api/prompts/:stage - Update stage config and/or template
-router.put('/:stage', asyncHandler(async (req, res) => {
- const { template, ...config } = req.body;
-
- if (Object.keys(config).length > 0) {
- await promptService.updateStageConfig(req.params.stage, config);
- }
- if (template !== undefined) {
- await promptService.updateStageTemplate(req.params.stage, template);
- }
- res.json({ success: true });
-}));
-
-// POST /api/prompts/:stage/preview - Preview compiled prompt
-router.post('/:stage/preview', asyncHandler(async (req, res) => {
- const { testData = {} } = req.body;
- const preview = await promptService.previewPrompt(req.params.stage, testData);
- res.json({ preview });
-}));
-
-// POST /api/prompts/reload - Reload prompts from disk
-router.post('/reload', asyncHandler(async (req, res) => {
- await promptService.loadPrompts();
- res.json({ success: true });
-}));
-
-export default router;
diff --git a/server/routes/providers.old.js b/server/routes/providers.old.js
deleted file mode 100644
index eb10ae4b..00000000
--- a/server/routes/providers.old.js
+++ /dev/null
@@ -1,134 +0,0 @@
-import { Router } from 'express';
-import { asyncHandler, ServerError } from '../lib/errorHandler.js';
-import * as providers from '../services/providers.js';
-import { testVision, runVisionTestSuite, checkVisionHealth } from '../services/visionTest.js';
-
-const router = Router();
-
-// GET /api/providers - List all providers
-router.get('/', asyncHandler(async (req, res) => {
- const data = await providers.getAllProviders();
- res.json(data);
-}));
-
-// GET /api/providers/active - Get active provider
-router.get('/active', asyncHandler(async (req, res) => {
- const provider = await providers.getActiveProvider();
- res.json(provider);
-}));
-
-// PUT /api/providers/active - Set active provider
-router.put('/active', asyncHandler(async (req, res) => {
- const { id } = req.body;
- if (!id) {
- throw new ServerError('Provider ID required', { status: 400, code: 'MISSING_ID' });
- }
-
- const provider = await providers.setActiveProvider(id);
-
- if (!provider) {
- throw new ServerError('Provider not found', { status: 404, code: 'NOT_FOUND' });
- }
-
- res.json(provider);
-}));
-
-// GET /api/providers/:id - Get provider by ID
-router.get('/:id', asyncHandler(async (req, res) => {
- const provider = await providers.getProviderById(req.params.id);
-
- if (!provider) {
- throw new ServerError('Provider not found', { status: 404, code: 'NOT_FOUND' });
- }
-
- res.json(provider);
-}));
-
-// POST /api/providers - Create new provider
-router.post('/', asyncHandler(async (req, res) => {
- const { name, type } = req.body;
-
- if (!name) {
- throw new ServerError('Name is required', { status: 400, code: 'VALIDATION_ERROR' });
- }
-
- if (!type || !['cli', 'api'].includes(type)) {
- throw new ServerError('Type must be "cli" or "api"', { status: 400, code: 'VALIDATION_ERROR' });
- }
-
- const provider = await providers.createProvider(req.body);
- res.status(201).json(provider);
-}));
-
-// PUT /api/providers/:id - Update provider
-router.put('/:id', asyncHandler(async (req, res) => {
- const provider = await providers.updateProvider(req.params.id, req.body);
-
- if (!provider) {
- throw new ServerError('Provider not found', { status: 404, code: 'NOT_FOUND' });
- }
-
- res.json(provider);
-}));
-
-// DELETE /api/providers/:id - Delete provider
-router.delete('/:id', asyncHandler(async (req, res) => {
- const deleted = await providers.deleteProvider(req.params.id);
-
- if (!deleted) {
- throw new ServerError('Provider not found', { status: 404, code: 'NOT_FOUND' });
- }
-
- res.status(204).send();
-}));
-
-// POST /api/providers/:id/test - Test provider connectivity
-router.post('/:id/test', asyncHandler(async (req, res) => {
- const result = await providers.testProvider(req.params.id);
- res.json(result);
-}));
-
-// POST /api/providers/:id/refresh-models - Refresh models for API provider
-router.post('/:id/refresh-models', asyncHandler(async (req, res) => {
- const provider = await providers.refreshProviderModels(req.params.id);
-
- if (!provider) {
- throw new ServerError('Provider not found or not an API type', { status: 404, code: 'NOT_FOUND' });
- }
-
- res.json(provider);
-}));
-
-// GET /api/providers/:id/vision-health - Check vision capability health
-router.get('/:id/vision-health', asyncHandler(async (req, res) => {
- const result = await checkVisionHealth(req.params.id);
- res.json(result);
-}));
-
-// POST /api/providers/:id/test-vision - Test vision with a specific image
-router.post('/:id/test-vision', asyncHandler(async (req, res) => {
- const { imagePath, prompt, expectedContent, model } = req.body;
-
- if (!imagePath) {
- throw new ServerError('imagePath is required', { status: 400, code: 'VALIDATION_ERROR' });
- }
-
- const result = await testVision({
- imagePath,
- prompt: prompt || 'Describe what you see in this image.',
- expectedContent: expectedContent || [],
- providerId: req.params.id,
- model
- });
-
- res.json(result);
-}));
-
-// POST /api/providers/:id/vision-suite - Run full vision test suite
-router.post('/:id/vision-suite', asyncHandler(async (req, res) => {
- const { model } = req.body;
- const result = await runVisionTestSuite(req.params.id, model);
- res.json(result);
-}));
-
-export default router;
diff --git a/server/routes/runs.old.js b/server/routes/runs.old.js
deleted file mode 100644
index bd7a5008..00000000
--- a/server/routes/runs.old.js
+++ /dev/null
@@ -1,191 +0,0 @@
-import { Router } from 'express';
-import * as runner from '../services/runner.js';
-import { asyncHandler, ServerError } from '../lib/errorHandler.js';
-
-const router = Router();
-
-// GET /api/runs - List runs
-// Query params: limit, offset, source (all|devtools|cos-agent)
-router.get('/', asyncHandler(async (req, res, next) => {
- const limit = parseInt(req.query.limit) || 50;
- const offset = parseInt(req.query.offset) || 0;
- const source = req.query.source || 'all'; // all, devtools, cos-agent
-
- const result = await runner.listRuns(limit, offset, source);
- res.json(result);
-}));
-
-// POST /api/runs - Create and execute a new run
-router.post('/', asyncHandler(async (req, res, next) => {
- const { providerId, model, prompt, workspacePath, workspaceName, timeout, screenshots } = req.body;
- console.log(`π POST /api/runs - provider: ${providerId}, model: ${model}, workspace: ${workspaceName}, timeout: ${timeout}ms, screenshots: ${screenshots?.length || 0}`);
-
- if (!providerId) {
- throw new ServerError('providerId is required', {
- status: 400,
- code: 'VALIDATION_ERROR'
- });
- }
-
- if (!prompt) {
- throw new ServerError('prompt is required', {
- status: 400,
- code: 'VALIDATION_ERROR'
- });
- }
-
- const runData = await runner.createRun({
- providerId,
- model,
- prompt,
- workspacePath,
- workspaceName,
- timeout,
- screenshots
- });
-
- const { runId, provider, metadata, timeout: effectiveTimeout } = runData;
- const io = req.app.get('io');
- console.log(`π Run created: ${runId}, provider type: ${provider.type}, command: ${provider.command}, timeout: ${effectiveTimeout}ms`);
-
- // Execute based on provider type
- if (provider.type === 'cli') {
- console.log(`π Executing CLI run: ${provider.command} with args: ${JSON.stringify(provider.args)}`);
- runner.executeCliRun(
- runId,
- provider,
- prompt,
- workspacePath,
- (data) => {
- // Stream output via Socket.IO
- console.log(`π€ Emitting run:${runId}:data (${data.length} chars)`);
- io?.emit(`run:${runId}:data`, data);
- },
- (finalMetadata) => {
- console.log(`β
Run complete: ${runId}, success: ${finalMetadata.success}`);
- io?.emit(`run:${runId}:complete`, finalMetadata);
- },
- effectiveTimeout
- );
- } else if (provider.type === 'api') {
- runner.executeApiRun(
- runId,
- provider,
- model,
- prompt,
- workspacePath,
- screenshots,
- (data) => {
- io?.emit(`run:${runId}:data`, data);
- },
- (finalMetadata) => {
- io?.emit(`run:${runId}:complete`, finalMetadata);
- }
- );
- }
-
- // Return immediately with run ID
- res.status(202).json({
- runId,
- status: 'started',
- metadata
- });
-}));
-
-// GET /api/runs/:id - Get run metadata
-router.get('/:id', asyncHandler(async (req, res, next) => {
- const metadata = await runner.getRun(req.params.id);
-
- if (!metadata) {
- throw new ServerError('Run not found', {
- status: 404,
- code: 'NOT_FOUND'
- });
- }
-
- const isActive = await runner.isRunActive(req.params.id);
- res.json({
- ...metadata,
- isActive
- });
-}));
-
-// GET /api/runs/:id/output - Get run output
-router.get('/:id/output', asyncHandler(async (req, res, next) => {
- const output = await runner.getRunOutput(req.params.id);
-
- if (output === null) {
- throw new ServerError('Run not found', {
- status: 404,
- code: 'NOT_FOUND'
- });
- }
-
- res.type('text/plain').send(output);
-}));
-
-// GET /api/runs/:id/prompt - Get run prompt
-router.get('/:id/prompt', asyncHandler(async (req, res, next) => {
- const prompt = await runner.getRunPrompt(req.params.id);
-
- if (prompt === null) {
- throw new ServerError('Run not found', {
- status: 404,
- code: 'NOT_FOUND'
- });
- }
-
- res.type('text/plain').send(prompt);
-}));
-
-// POST /api/runs/:id/stop - Stop a running execution
-router.post('/:id/stop', asyncHandler(async (req, res, next) => {
- const stopped = await runner.stopRun(req.params.id);
-
- if (!stopped) {
- throw new ServerError('Run not found or not active', {
- status: 404,
- code: 'NOT_ACTIVE'
- });
- }
-
- res.json({ stopped: true });
-}));
-
-// DELETE /api/runs/:id - Delete run and artifacts
-router.delete('/:id', asyncHandler(async (req, res, next) => {
- // Don't allow deleting active runs
- const isActive = await runner.isRunActive(req.params.id);
- if (isActive) {
- throw new ServerError('Cannot delete active run', {
- status: 409,
- code: 'RUN_ACTIVE'
- });
- }
-
- const deleted = await runner.deleteRun(req.params.id);
-
- if (!deleted) {
- throw new ServerError('Run not found', {
- status: 404,
- code: 'NOT_FOUND'
- });
- }
-
- res.status(204).send();
-}));
-
-// DELETE /api/runs - Delete all failed runs
-// Requires ?confirm=true query parameter to prevent accidental deletion
-router.delete('/', asyncHandler(async (req, res, next) => {
- if (req.query.confirm !== 'true') {
- throw new ServerError('Destructive operation requires ?confirm=true', {
- status: 400,
- code: 'CONFIRMATION_REQUIRED'
- });
- }
- const deletedCount = await runner.deleteFailedRuns();
- res.json({ deleted: deletedCount });
-}));
-
-export default router;
diff --git a/server/services/agents.js b/server/services/agents.js
index 0b502dbb..eebce978 100644
--- a/server/services/agents.js
+++ b/server/services/agents.js
@@ -181,7 +181,7 @@ async function findWindowsProcesses(pattern) {
// Skip header line
for (let i = 1; i < lines.length; i++) {
const parts = lines[i].split(',');
- if (parts.length >= 6) {
+ if (parts.length >= 7) {
const command = parts[1];
const creationDate = parts[2];
const ppid = parseInt(parts[3]);
diff --git a/server/services/apps.js b/server/services/apps.js
index 999ac9e6..588145cd 100644
--- a/server/services/apps.js
+++ b/server/services/apps.js
@@ -1,9 +1,8 @@
-import { readFile, writeFile } from 'fs/promises';
-import { existsSync } from 'fs';
+import { writeFile } from 'fs/promises';
import { join } from 'path';
import { v4 as uuidv4 } from 'uuid';
import EventEmitter from 'events';
-import { ensureDir, PATHS } from '../lib/fileUtils.js';
+import { ensureDir, readJSONFile, PATHS } from '../lib/fileUtils.js';
const DATA_DIR = PATHS.data;
const APPS_FILE = join(DATA_DIR, 'apps.json');
@@ -36,14 +35,7 @@ async function loadApps() {
await ensureDataDir();
- if (!existsSync(APPS_FILE)) {
- appsCache = { apps: {} };
- cacheTimestamp = now;
- return appsCache;
- }
-
- const content = await readFile(APPS_FILE, 'utf-8');
- appsCache = JSON.parse(content);
+ appsCache = await readJSONFile(APPS_FILE, { apps: {} });
cacheTimestamp = now;
return appsCache;
}
@@ -103,7 +95,7 @@ export async function getActiveApps() {
*/
export async function getAppById(id) {
const data = await loadApps();
- const app = data.apps[id];
+ const app = data?.apps?.[id];
return app ? { id, ...app } : null;
}
diff --git a/server/services/autonomousJobs.test.js b/server/services/autonomousJobs.test.js
index f3f42e99..ea5a8646 100644
--- a/server/services/autonomousJobs.test.js
+++ b/server/services/autonomousJobs.test.js
@@ -65,10 +65,10 @@ describe('autonomousJobs', () => {
it('never-run enabled job is always due', async () => {
const due = await getDueJobs()
- expect(due).toHaveLength(1)
- expect(due[0].id).toBe('job-test-1')
- expect(due[0].reason).toBe('never-run')
- expect(due[0].overdueBy).toBeGreaterThan(0)
+ const testJob = due.find(j => j.id === 'job-test-1')
+ expect(testJob).toBeDefined()
+ expect(testJob.reason).toBe('never-run')
+ expect(testJob.overdueBy).toBeGreaterThan(0)
})
it('recently-run job is NOT due', async () => {
@@ -85,7 +85,7 @@ describe('autonomousJobs', () => {
const due = await getDueJobs()
- expect(due).toHaveLength(0)
+ expect(due.find(j => j.id === 'job-test-1')).toBeUndefined()
})
it('jobs sort by most overdue first', async () => {
@@ -115,10 +115,18 @@ describe('autonomousJobs', () => {
const due = await getDueJobs()
- expect(due).toHaveLength(2)
- expect(due[0].id).toBe('job-2')
- expect(due[1].id).toBe('job-1')
- expect(due[0].overdueBy).toBeGreaterThan(due[1].overdueBy)
+ // Verify relative ordering: more overdue jobs should come first
+ const job2Idx = due.findIndex(j => j.id === 'job-2')
+ const job1Idx = due.findIndex(j => j.id === 'job-1')
+ expect(job2Idx).toBeGreaterThanOrEqual(0)
+ expect(job1Idx).toBeGreaterThanOrEqual(0)
+ expect(job2Idx).toBeLessThan(job1Idx)
+ expect(due[job2Idx].overdueBy).toBeGreaterThan(due[job1Idx].overdueBy)
+
+ // Verify only expected test jobs plus any default jobs are present
+ const testJobIds = new Set(['job-test-1', 'job-1', 'job-2'])
+ const unexpectedJobs = due.filter(j => !testJobIds.has(j.id) && !j.id.startsWith('job-github-'))
+ expect(unexpectedJobs).toHaveLength(0)
})
})
diff --git a/server/services/commands.js b/server/services/commands.js
index 53848dcc..4fda189d 100644
--- a/server/services/commands.js
+++ b/server/services/commands.js
@@ -147,22 +147,3 @@ export function getAllowedCommands() {
return Array.from(ALLOWED_COMMANDS).sort();
}
-/**
- * Add a command to the allowlist (runtime only)
- */
-export function addAllowedCommand(command) {
- ALLOWED_COMMANDS.add(command);
-}
-
-/**
- * Remove a command from the allowlist (runtime only)
- */
-export function removeAllowedCommand(command) {
- // Don't allow removing core commands
- const core = ['npm', 'node', 'git', 'pm2'];
- if (core.includes(command)) {
- return false;
- }
- ALLOWED_COMMANDS.delete(command);
- return true;
-}
diff --git a/server/services/pm2.js b/server/services/pm2.js
index fc09c29a..091cfc04 100644
--- a/server/services/pm2.js
+++ b/server/services/pm2.js
@@ -1,6 +1,7 @@
import pm2 from 'pm2';
import { spawn } from 'child_process';
import { existsSync } from 'fs';
+import { extractJSONArray, safeJSONParse } from '../lib/fileUtils.js';
/**
* Build environment object with optional custom PM2_HOME
@@ -18,6 +19,29 @@ function buildEnv(pm2Home) {
return env;
}
+/**
+ * Spawn a PM2 CLI command with optional custom PM2_HOME
+ * @param {string} action PM2 action (stop, restart, delete)
+ * @param {string} name PM2 process name
+ * @param {string} pm2Home Optional custom PM2_HOME path
+ * @returns {Promise<{success: boolean}>}
+ */
+function spawnPm2Cli(action, name, pm2Home) {
+ return new Promise((resolve, reject) => {
+ const child = spawn('pm2', [action, name], {
+ shell: false,
+ env: buildEnv(pm2Home)
+ });
+ let stderr = '';
+ child.stderr.on('data', (data) => { stderr += data.toString(); });
+ child.on('close', (code) => {
+ if (code !== 0) return reject(new Error(stderr || `pm2 ${action} exited with code ${code}`));
+ resolve({ success: true });
+ });
+ child.on('error', reject);
+ });
+}
+
/**
* Connect to PM2 daemon and run an action
* Note: This uses the default PM2_HOME. For custom PM2_HOME, use CLI commands.
@@ -75,19 +99,7 @@ export async function startApp(name, options = {}) {
export async function stopApp(name, pm2Home = null) {
// Use CLI for custom PM2_HOME
if (pm2Home) {
- return new Promise((resolve, reject) => {
- const child = spawn('pm2', ['stop', name], {
- shell: false,
- env: buildEnv(pm2Home)
- });
- let stderr = '';
- child.stderr.on('data', (data) => { stderr += data.toString(); });
- child.on('close', (code) => {
- if (code !== 0) return reject(new Error(stderr || `pm2 stop exited with code ${code}`));
- resolve({ success: true });
- });
- child.on('error', reject);
- });
+ return spawnPm2Cli('stop', name, pm2Home);
}
return connectAndRun((pm2) => {
@@ -108,19 +120,7 @@ export async function stopApp(name, pm2Home = null) {
export async function restartApp(name, pm2Home = null) {
// Use CLI for custom PM2_HOME
if (pm2Home) {
- return new Promise((resolve, reject) => {
- const child = spawn('pm2', ['restart', name], {
- shell: false,
- env: buildEnv(pm2Home)
- });
- let stderr = '';
- child.stderr.on('data', (data) => { stderr += data.toString(); });
- child.on('close', (code) => {
- if (code !== 0) return reject(new Error(stderr || `pm2 restart exited with code ${code}`));
- resolve({ success: true });
- });
- child.on('error', reject);
- });
+ return spawnPm2Cli('restart', name, pm2Home);
}
return connectAndRun((pm2) => {
@@ -141,19 +141,7 @@ export async function restartApp(name, pm2Home = null) {
export async function deleteApp(name, pm2Home = null) {
// Use CLI for custom PM2_HOME
if (pm2Home) {
- return new Promise((resolve, reject) => {
- const child = spawn('pm2', ['delete', name], {
- shell: false,
- env: buildEnv(pm2Home)
- });
- let stderr = '';
- child.stderr.on('data', (data) => { stderr += data.toString(); });
- child.on('close', (code) => {
- if (code !== 0) return reject(new Error(stderr || `pm2 delete exited with code ${code}`));
- resolve({ success: true });
- });
- child.on('error', reject);
- });
+ return spawnPm2Cli('delete', name, pm2Home);
}
return connectAndRun((pm2) => {
@@ -184,18 +172,7 @@ export async function getAppStatus(name, pm2Home = null) {
});
child.on('close', () => {
- // pm2 jlist may output ANSI codes and warnings before JSON
- let jsonStart = stdout.indexOf('[{');
- if (jsonStart < 0) {
- jsonStart = stdout.lastIndexOf('[]');
- }
- const pm2Json = jsonStart >= 0 ? stdout.slice(jsonStart) : '[]';
- let processes;
- try {
- processes = JSON.parse(pm2Json);
- } catch {
- return resolve({ name, status: 'error', pm2_env: null });
- }
+ const processes = safeJSONParse(extractJSONArray(stdout), []);
const proc = processes.find(p => p.name === name);
if (!proc) {
@@ -242,15 +219,7 @@ export async function listProcesses(pm2Home = null) {
});
child.on('close', () => {
- // pm2 jlist may output ANSI codes and warnings before JSON
- // Look for '[{' (array with objects) or '[]' (empty array) to avoid matching ANSI codes like [31m
- let jsonStart = stdout.indexOf('[{');
- if (jsonStart < 0) {
- const emptyMatch = stdout.match(/\[\](?![0-9])/);
- jsonStart = emptyMatch ? stdout.indexOf(emptyMatch[0]) : -1;
- }
- const pm2Json = jsonStart >= 0 ? stdout.slice(jsonStart) : '[]';
- const list = JSON.parse(pm2Json);
+ const list = safeJSONParse(extractJSONArray(stdout), []);
const processes = list.map(proc => ({
name: proc.name,
status: proc.pm2_env?.status || 'unknown',
diff --git a/server/services/scriptRunner.js b/server/services/scriptRunner.js
index 233b31c7..34daeef8 100644
--- a/server/services/scriptRunner.js
+++ b/server/services/scriptRunner.js
@@ -8,11 +8,12 @@
import { spawn } from 'child_process';
import { join, dirname } from 'path';
import { fileURLToPath } from 'url';
-import { writeFile, readFile, mkdir, readdir, rm } from 'fs/promises';
import { existsSync } from 'fs';
+import { writeFile, mkdir, readdir, rm } from 'fs/promises';
import { v4 as uuidv4 } from 'uuid';
import Cron from 'croner';
import { cosEvents } from './cosEvents.js';
+import { readJSONFile } from '../lib/fileUtils.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -43,6 +44,19 @@ const ALLOWED_SCRIPT_COMMANDS = new Set([
// Security: Reject any command containing these to prevent injection via pipes, chaining, etc.
const DANGEROUS_SHELL_CHARS = /[;|&`$(){}[\]<>\\!#*?~]/;
+// Patterns matching sensitive environment variable values in command output (e.g., pm2 jlist)
+// Keys must be underscore-delimited segments to avoid false positives like "monkey" or "keymetrics"
+const SENSITIVE_ENV_PATTERN = /("(?:[a-z0-9]+_)*(?:KEY|SECRET|TOKEN|PASSWORD|PASSPHRASE|MACAROON|CERT|CREDENTIAL|AUTH)(?:_[a-z0-9]+)*":\s*)"[^"]+"/gi;
+
+/**
+ * Redact sensitive env vars from command output before persisting.
+ * Matches JSON key-value pairs where the key contains secret-like words.
+ */
+function redactSensitiveOutput(output) {
+ if (!output) return output;
+ return output.replace(SENSITIVE_ENV_PATTERN, '$1"[REDACTED]"');
+}
+
/**
* Validate a script command against the allowlist
* Returns { valid: boolean, error?: string, baseCommand?: string, args?: string[] }
@@ -126,11 +140,7 @@ async function ensureScriptsDir() {
* Load scripts state
*/
async function loadScriptsState() {
- if (!existsSync(SCRIPTS_STATE_FILE)) {
- return { scripts: {} };
- }
- const content = await readFile(SCRIPTS_STATE_FILE, 'utf-8');
- return JSON.parse(content);
+ return readJSONFile(SCRIPTS_STATE_FILE, { scripts: {} });
}
/**
@@ -363,10 +373,11 @@ export async function executeScript(scriptId) {
child.on('close', async (code) => {
const duration = Date.now() - startTime;
const fullOutput = output + (error ? `\n[stderr]\n${error}` : '');
+ const redactedOutput = redactSensitiveOutput(fullOutput);
// Update script state
script.lastRun = new Date().toISOString();
- script.lastOutput = fullOutput.substring(0, 10000); // Limit stored output
+ script.lastOutput = redactedOutput.substring(0, 10000);
script.lastExitCode = code;
script.runCount = (script.runCount || 0) + 1;
@@ -391,7 +402,7 @@ export async function executeScript(scriptId) {
description: script.triggerPrompt,
taskType: 'internal',
metadata: {
- context: `Script Output:\n\`\`\`\n${fullOutput.substring(0, 5000)}\n\`\`\``,
+ context: `Script Output:\n\`\`\`\n${redactedOutput.substring(0, 5000)}\n\`\`\``,
source: 'script',
scriptId,
scriptName: script.name
@@ -404,13 +415,13 @@ export async function executeScript(scriptId) {
name: script.name,
exitCode: code,
duration,
- outputLength: fullOutput.length
+ outputLength: redactedOutput.length
});
resolve({
success: code === 0,
exitCode: code,
- output: fullOutput,
+ output: redactedOutput,
duration
});
});
diff --git a/server/services/socialAccounts.js b/server/services/socialAccounts.js
index 94c6edb9..5b61d4ad 100644
--- a/server/services/socialAccounts.js
+++ b/server/services/socialAccounts.js
@@ -9,12 +9,11 @@
* - Future account management automation
*/
-import { readFile, writeFile } from 'fs/promises';
-import { existsSync } from 'fs';
+import { writeFile } from 'fs/promises';
import { join } from 'path';
import { v4 as uuidv4 } from 'uuid';
import EventEmitter from 'events';
-import { ensureDir, PATHS } from '../lib/fileUtils.js';
+import { ensureDir, readJSONFile, PATHS } from '../lib/fileUtils.js';
const DATA_FILE = join(PATHS.digitalTwin, 'social-accounts.json');
@@ -135,14 +134,7 @@ async function loadAccounts() {
await ensureDir(PATHS.digitalTwin);
- if (!existsSync(DATA_FILE)) {
- cache = { accounts: {} };
- cacheTimestamp = now;
- return cache;
- }
-
- const content = await readFile(DATA_FILE, 'utf-8');
- cache = JSON.parse(content);
+ cache = await readJSONFile(DATA_FILE, { accounts: {} });
cacheTimestamp = now;
return cache;
}