# Project export: Egomorph-CORE-

This document was generated by HackStack to give an AI agent context about a hackathon project. Sections are labeled with their provenance; content marked as truncated was cut to keep this document small.

## Project metadata

- Hackathon: OpenAI Build Week
- Tagline: Do not rely on blind trust, but on transparent and controlled execution.
- Devpost: https://devpost.com/software/egomorph-core
- GitHub: https://github.com/Mcpasi/Egomorph-CORE-
- Video: https://www.youtube.com/embed/KSz9E_siEGY?enablejsapi=1&hl=en_US&rel=0&start=&version=3&wmode=transparent
- Team: 1 GitHub contributor(s) — Mcpasi (2 commits)

## Devpost submission (written by the team)

### Overview

A local control center for agentic AI: one installable browser interface, three interchangeable model paths, and a transparent permission-based skill system. Deutsche Version · Detailed documentation · MIT License Egomorph Core brings together a local browser LLM, OpenAI-compatible APIs, and the official Codex CLI in a single PWA. In the agentic API and Codex profiles, the active model decides semantically whether it needs an approved skill. Every real access is visible, permissions are checked before execution, and private working files stay inside a restricted local model home. In one sentence: Egomorph Core turns different AI backends into an observable local agent—without rule-based response templates, simulated tool calls, or a cloud dependency for the user interface. Why this project stands out One interface, three model paths: run locally in the browser, through an OpenAI-compatible API, or through the official Codex CLI. Observable agent runs: reasoning summary, genuine skill accesses, and the final answer are presented as separate live stages. Least-privilege skills: installation, activation, profiles, permissions, and setup are driven by manifests. Source provenance instead of decorative citations: only web sources actually passed to the model may support the final answer. A local security boundary: memory and files live in the model home; path traversal, secrets, and protected directories remain blocked. An installable PWA: local conversations, an offline app shell, a Writer agent, and a responsive desktop/mobile interface. How it works Only one skill may be requested per model step. For complex tasks, the agent can perform several approved accesses in sequence; the current limit is six accesses per user turn. The displayed reasoning is a short, outcome-focused summary—not private chain-of-thought. Requirements and dependencies Required The project's only npm development dependencies are Jest and TypeScript. Normal API or Codex use requires neither Python nor Docker. Required only for the selected profile Install the Codex CLI: This installation method follows the official OpenAI Codex example. Egomorph Core never imports cookies, access tokens, or auth.json; authentication is delegated exclusively to the official CLI. Quick start The dashboard then opens at http://localhost:8787/ by default. If the normal browser login is unavailable: Useful commands: Codex authentication is needed only for the Codex profile. Skip that step when using only a local model or an API endpoint. The three profiles Installing your own skill in Egomorph Core What “install” currently means For security, Egomorph Core does not load arbitrary skill files uploaded through the browser. A custom skill must first be registered once in the codebase. It then appears in the skill catalog, where any user can install, enable, configure, assign profiles, and grant permissions entirely through Settings → Skills. A skill needs at least: a manifest at skills/<skill-id>/manifest.json, a browser entrypoint such as skills/<skill-id>/index.js, a runtime adapter in the generative agent loop, static-file approval in the gateway and PWA, translations and tests for the integration. 1. Create the manifest Required fields are schemaVersion, id, name, version, entrypoint, profiles, and permissions. Keep id stable and unique, and use semantic versioning for version. Optional setup fields automatically become inputs on the skill card. A setup field tied to a permission is passed to a run only when that permission is granted. Valid profiles are: full — local browser model; the registry accepts this profile, but a custom skill also needs a local tool loop. The current built-in skills target api and codex. api — OpenAI-compatible endpoint codex — official Codex CLI integration 2. Implement the entrypoint The browser code named by entrypoint exposes a clearly named runtime API. It should validate all inputs, support AbortSignal when possible, return only the minimum necessary context, and obtain run configuration through EgoSkillSystem.getConfigForRun(id). Never place secrets in a manifest or source file. The built-in implementations demonstrate the two common patterns: skills/internetSkill.js — network access and normalized sources skills/extendedFileSkill.js — permission-separated gateway operations skills/learnWithEgomorphSkill.js — adaptive tutor rules without canned answers 3. Register the manifest Add the new manifest path to MANIFEST_URLS in skillSystem.js. Only known manifests are validated and displayed in the skill catalog. 4. Connect it to the agent loop The manifest describes management and permissions, but not execution semantics. resourceProfile.js therefore needs explicit support for: the skill's structured <egomorph_skill_request> shape, strict validation of every allowed parameter, availability and permission checks, the runtime call itself, bounded, sanitized context for the next model step, genuine onSkillStart, onSkillUse, onSkillError, or blocked events. Unknown or invalid model requests must never execute. A skill is available only when installed and enabled, allowed for the current profile, and granted every required permission. 5. Approve files for the gateway and PWA Add the manifest and entrypoint to DEFAULT_DASHBOARD_FILES in scripts/codex-bridge.js and to URLS_TO_CACHE in sw.js, then increment CACHE_NAME in sw.js. Otherwise the gateway may reject the files or an installed PWA may continue to use a stale app shell. 6. Add UI copy and tests New translation keys must exist with identical key sets in translations/de.js, translations/en.js, and translations/fr.js. At minimum, test: manifest validation and default state, installation, activation, profiles, and permissions, allowed, blocked, and failed execution, input validation and cancellation, sanitization of context passed to the model. 7. Install it in the browser After restarting ./egomorph dashboard: Open Settings → Skills. Select Install on the new skill. Turn on Enable. Select the allowed profiles. Grant only the required permissions. Complete and save any manifest-defined setup fields. Test with a request that semantically requires the skill. Persistent skill state remains local in the browser under egoSkillStatesV1. Uninstalling a skill blocks new runs immediately. Built-in skills Internet Research internet.research uses Google Programmable Search or fallback providers. Network access is required; local Google credentials have a separate optional permission. The UI reports the number of sources actually prepared for the final model step. Extended Project Files workspace.extended-files is disabled by default. Read and write permissions are granted separately. Even when approved, it allows only .js, .css, .html, and .py inside the model home; .env*, .git, node_modules, outside paths, and escaping symlinks remain blocked. Learn with EgoMorph learning.egomorph is built in for API and Codex and needs no additional permissions. It starts an adaptive, playful JavaScript and TypeScript learning loop through EgoMorph architecture: when the level is unknown, the model asks for it first, then generates suitable explanations, quizzes, debugging and implementation tasks, hints, and feedback from the current conversation. No answer or solution catalogue is hard-coded. Exact code access remains the job of separately approved skills. Security and privacy The gateway binds to 127.0.0.1:8787 by default. Other browser origins must be explicitly allowed through CODEX_BRIDGE_ALLOWED_ORIGINS. The model home is <project folder>/EgomorphCore/model-home. memory.md is reserved for persistent memory. Browser uploads accept controlled Markdown context. Internal files, paths, raw contents, system prompts, and secrets are not emitted as agent replies. Gateway and API routes bypass the service-worker cache. Never expose the gateway publicly without an authentication layer in front of it. Development and verification Key entrypoints: See DOKUMENTATION_EN.md for the detailed technical reference. Egomorph Core is released under the MIT License.

## README (from the GitHub repository)

# Egomorph Core

## Codex

The entire skill system was created using Codex (GPT 5.6).



> **A local control center for agentic AI:** one installable browser interface, three interchangeable model paths, and a transparent permission-based skill system.

[Deutsche Version](README_DE.md) · [Detailed documentation](DOKUMENTATION_EN.md) · MIT License

Egomorph Core brings together a local browser LLM, OpenAI-compatible APIs, and the official Codex CLI in a single PWA. In the agentic API and Codex profiles, the active model decides semantically whether it needs an approved skill. Every real access is visible, permissions are checked before execution, and private working files stay inside a restricted local model home.

**In one sentence:** Egomorph Core turns different AI backends into an observable local agent—without rule-based response templates, simulated tool calls, or a cloud dependency for the user interface.

## Why this project stands out

- **One interface, three model paths:** run locally in the browser, through an OpenAI-compatible API, or through the official Codex CLI.
- **Observable agent runs:** reasoning summary, genuine skill accesses, and the final answer are presented as separate live stages.
- **Least-privilege skills:** installation, activation, profiles, permissions, and setup are driven by manifests.
- **Source provenance instead of decorative citations:** only web sources actually passed to the model may support the final answer.
- **A local security boundary:** memory and files live in the model home; path traversal, secrets, and protected directories remain blocked.
- **An installable PWA:** local conversations, an offline app shell, a Writer agent, and a responsive desktop/mobile interface.

## How it works

```mermaid
flowchart LR
    U["User request"] --> UI["Egomorph Core PWA"]
    UI --> M{"Active profile"}
    M --> L["Local browser LLM"]
    M --> A["OpenAI-compatible API"]
    M --> C["Official Codex CLI"]
    L --> R
    A --> D
    C --> D
    D -->|"No"| R["Final answer"]
    D -->|"Yes"| P["Check installation, profile, and permissions"]
    P -->|"approved"| S["Run skill and return sanitized context"]
    P -->|"blocked"| B["Show access as not started"]
    S --> D
    B --> R
```

Only one skill may be requested per model step. For complex tasks, the agent can perform several approved accesses in sequence; the current limit is six accesses per user turn. The displayed reasoning is a short, outcome-focused summary—not private chain-of-thought.

## Requirements and dependencies

### Required

| Dependency | Purpose |
| --- | --- |
| [Node.js](https://nodejs.org/) with npm | Runs the dashboard and local gateway. A current LTS release is recommended. |
| Modern browser | Runs the PWA; Chrome or Edge generally provides the best compatibility for local models. |
| Internet access during setup | Downloads npm packages and, when selected, Transformers.js and a browser model. |

The project's only npm development dependencies are Jest and TypeScript. Normal API or Codex use requires neither Python nor Docker.

### Required only for the selected profile

| Profile/feature | Additional requirement |
| --- | --- |
| **Local** | A compatible Hugging Face model ID; download size, RAM use, and performance depend on the model and device. |
| **API** | An OpenAI-compatible endpoint URL, a model name, and an API key when required. OpenAI, OpenRouter, Ollama, and LM Studio are among the supported options. |
| **Codex** | The official Codex CLI and a valid login. |
| **Google research** | Optional Google Programmable Search API key and Search Engine ID. Without them, the internet skill uses fallback providers. |

Install the Codex CLI:

```bash
npm install -g @openai/codex@latest
codex --version
```

This installation method follows the [official OpenAI Codex example](https://developers.openai.com/cookbook/examples/codex/secure_quality_gitlab#code-quality-cicd-job-example). Egomorph Core never imports cookies, access tokens, or `auth.json`; authentication is delegated exclusively to the official CLI.

## Quick start

```bash
npm install
./egomorph codex login
./egomorph dashboard
```

The dashboard then opens at [http://localhost:8787/](http://localhost:8787/) by default.

If the normal browser login is unavailable:

```bash
./egomorph codex login --device-auth
```

Useful commands:

```bash
./egomorph codex status   # Check authentication
./egomorph dashboard      # Start gateway and open the browser
./egomorph gateway        # Start gateway without opening the browser
./egomorph clean          # Clear the app cache and service worker
```

> Codex authentication is needed only for the Codex profile. Skip that step when using only a local model or an API endpoint.

## The three profiles

| Profile | Response source | Best suited for |
| --- | --- | --- |
| **Local** (`full`) | Transformers.js model running in the browser | Privacy, offline use after model download, and on-device experimentation |
| **API** (`api`) | OpenAI-compatible Chat Completions endpoint | Provider/model choice, local servers, or hosted APIs |
| **Codex** (`codex`) | Official Codex CLI through the local App Server | Agentic tasks, dynamic model discovery, streaming, and Codex web search |

## Installing your own skill in Egomorph Core

### What “install” currently means

For security, Egomorph Core does not load arbitrary skill files uploaded through the browser. A custom skill must first be registered once in the codebase. It then appears in the skill catalog, where any user can install, enable, configure, assign profiles, and grant permissions entirely through **Settings → Skills**.

A skill needs at least:

1. a manifest at `skills/<skill-id>/manifest.json`,
2. a browser entrypoint such as `skills/<skill-id>/index.js`,
3. a runtime adapter in the generative agent loop,
4. static-file approval in the gateway and PWA,
5. translations and tests for the integration.

### 1. Create the manifest

```json
{
  "schemaVersion": 1,
  "id": "example.my-skill",
  "name": "My Skill",
  "displayNameKey": "mySkillName",
  "descriptionKey": "mySkillDescription",
  "version": "1.0.0",
  "entrypoint": "skills/my-skill/index.js",
  "builtIn": false,
  "defaultEnabled": false,
  "profiles": ["api", "codex"],
  "permissions": [
    {
      "id": "network",
      "labelKey": "skillPermissionNetwork",
      "descriptionKey": "skillPermissionNetworkDescription",
      "required": true,
      "defaultGranted": false
    }
  ],
  "setup": [
    {
      "id": "endpoint",
      "type": "text",
      "labelKey": "mySkillEndpointLabel"
    }
  ]
}
```

Required fields are `schemaVersion`, `id`, `name`, `version`, `entrypoint`, `profiles`, and `permissions`. Keep `id` stable and unique, and use semantic versioning for `version`. Optional `setup` fields automatically become inputs on the skill card. A setup field tied to a `permission` is passed to a run only when that permission is granted.

Valid profiles are:

- `full` — local browser model; the registry accepts this profile, but a custom skill also needs a local tool loop. The current built-in skills target `api` and `codex`.
- `api` — OpenAI-compatible endpoint
- `codex` — official Codex CLI integration

### 2. Implement the entrypoint

The browser code named by `entrypoint` exposes a clearly named runtime API. It should validate all inputs, support `AbortSignal` when possible, return only the minimum necessary context, and obtain run configuration through `EgoSkillSystem.getConfigForRun(id)`. Never place secrets in a manifest or source file.

The built-in implementations demonstrate the two common patterns:

- [`skills/internetSkill.js`](skills/internetSkill.js) — network access and normalized sources
- [`skills/extendedFileSkill.js`](skills/extendedFileSkill.js) — permission-separated gateway operations
- [`skills/learnWithEgomorphSkill.js`](skills/learnWithEgomorphSkill.js) — adaptive tutor rules without canned answers

### 3. Regis

[README truncated for size]

## Detected evidence (automated analysis)

Indexed codebase: 57 recognized source files, 544 KB.
- CSS (language) — detected in the code
- HTML (language) — detected in the code
- JavaScript (language) — detected in the code
- TypeScript (language) — detected in the code

## Codebase structure (from repository index)

### Files (67 of 67)

```
agentResponse.js
app.js
CHANGELOG.md
chatModel.js
conversationStore.js
docs/image/Test.js
doku.md
DOKUMENTATION_EN.md
DOKUMENTATION.md
egomorph
EgomorphCore/model-home/README.md
index.html
LICENSE
load-screen.css
loader.js
ltmManager.js
manifest.json
package.json
PWABUILDER.md
README_DE.md
README.md
resourceProfile.js
Safetyfilter.js
Safetyfilter.ts
scripts/codex-app-server-client.js
scripts/codex-bridge.js
scripts/codex-login-persistent.js
scripts/egomorph-gateway.js
scripts/egomorph.js
scripts/generate-core-icons.js
scripts/validate-pwa.js
skills/extended-files/manifest.json
skills/extendedFileSkill.js
skills/internet/manifest.json
skills/internetSkill.js
skills/learn-with-egomorph/agents/openai.yaml
skills/learn-with-egomorph/manifest.json
skills/learn-with-egomorph/SKILL.md
skills/learnWithEgomorphSkill.js
skillSystem.js
style.css
sw.js
tests/agentResponse.test.js
tests/chatModel.test.js
tests/codexAppServerClient.test.js
tests/codexBridge.test.js
tests/conversationStore.test.js
tests/docsFormat.test.js
tests/egomorphGateway.test.js
tests/extendedFileSkill.test.js
tests/indexGuards.test.js
tests/internetSkill.test.js
tests/learnWithEgomorphSkill.test.js
tests/ltmManager.test.js
tests/Main.txt
tests/resourceProfile.test.js
tests/serviceWorkerUpdate.test.js
tests/skillSystem.test.js
tests/translations.test.js
tests/writer.test.js
translations/de.js
translations/en.js
translations/fr.js
translations/Test.js
translations/Text.txt
tsconfig.json
Writer.js
```

### Dependencies

- package.json: jest@^29.7.0, typescript@^5.9.3

### Recent commits (newest first)

- Fix formatting of Codex section in README
- Add Codex section to README
- Update version in loader.js configuration
- Update version number in index.html
- Rename README_EN.md to README.md
- Update README_DE.md links and content
- Delete EgomorphCore/model-home/Test.txt
- Delete EgomorphCore/model-home/CHANGELOG.md
- Add files via upload
- Add files via upload
- Add files via upload
- Delete skills/learn-with-egomorph/agents/Text.txt
- Add files via upload
- Create Text.txt
- Delete skills/learn-with-egomorph/Test.txt
- Add files via upload
- Create Test.txt
- Delete skills/extended-files/Test.txt
- Add files via upload
- Create Test.txt

## Key source files (fetched from GitHub, selected and truncated for size)

### PWABUILDER.md

```markdown
# PWABuilder Vorbereitung

Dieses Verzeichnis ist die PWA-faehige App-Shell fuer den Export als Android APK/AAB. Lokal wird Egomorph Core bevorzugt ueber das Egomorph-Core-Gateway gestartet; fuer PWABuilder bleibt eine HTTPS-Auslieferung der statischen App-Shell noetig.

## Vor dem Upload

1. Projekt auf HTTPS hosten, zum Beispiel GitHub Pages, Netlify, Vercel oder eigener Server.
2. Die gehostete URL in PWABuilder eintragen.
3. In PWABuilder den Android-Export waehlen.
4. Fuer Play Store Release `AAB` verwenden, fuer direkte Installation/Test `APK`.

## Lokale Checks

```bash
npm install
npm run build:safetyfilter
npm run pwa:validate
./egomorph gateway
```

Danach im Browser `http://localhost:8787` oeffnen. PWABuilder selbst muss spaeter eine HTTPS-URL pruefen.

## Android Asset Links

PWABuilder erzeugt fuer Trusted Web Activity ein eigenes `assetlinks.json`, sobald Paketname und Signatur bekannt sind. Die Datei muss danach unter:

```text
https://deine-domain.example/.well-known/assetlinks.json
```

ausgeliefert werden. Ohne diese finale Datei startet Android die App ggf. mit Browser-Leiste statt als voll vertrauenswuerdige TWA.

```

### DOKUMENTATION_EN.md

```markdown
# Egomorph Core – technical summary

This file summarizes the current architecture. The detailed canonical reference is `doku.md`.

Egomorph Core has three generative profiles: a local browser LLM (`full`), an OpenAI-compatible API (`api`), and the official Codex CLI through the local gateway (`codex`). Chat orchestration lives in `app.js`, profiles and gateway dispatch in `resourceProfile.js`, and local text generation in `chatModel.js`.

`agentResponse.js` displays every turn immediately as a live flow and separates a short safe reasoning summary from the final answer. Codex and streaming APIs update both areas token by token. A skill step appears only for an actual runtime access; multiple accesses are shown as separate ordered rows under step 2, including repeated uses of the same skill. Statuses cover running, blocked, technical failure, file read/write, and research-source results. With zero sources, the final model step must not output citations or evidentiary URLs. Private chain-of-thought and internal model-home files, names, paths, or raw contents are not displayed.

Codex-native `webSearch` items are shown as a separate runtime access from their App Server lifecycle events. They are neither discarded nor mixed with the browser skill's result count.

The active model makes skill decisions semantically without keyword rules. Each model step may emit one structured call; after execution it may request another skill, up to six accesses, or provide the final answer. A request blocked by enablement, profile, entrypoint, or permission state is visibly reported as `not started`.

`skillSystem.js` manages skills from individual JSON manifests. The internet manifest at `skills/internet/manifest.json` defines research and network permissions. `skills/extended-files/manifest.json` defines the disabled-by-default `workspace.extended-files` skill with separate read and write permissions for `.js`, `.css`, `.html`, and `.py` inside the model home. `learning.egomorph` is displayed as **Learn with EgoMorph** and starts an adaptive JavaScript/TypeScript learning session through EgoMorph architecture for API/Codex. It first asks for the learner's level when unknown, while explanations, quizzes, tasks, hints, and feedback are generated by the model from the current conversation rather than stored as canned answers. Traversal, protected directories, and escaping symlinks remain blocked. Installation, enablement, profile assignments, permissions, configuration, and last-run data stay in the browser and are managed under `Settings -> Skills`.

Conversations are isolated in `egoConversationThreads`. In Codex mode each thread ID is forwarded as `egomorph.sessionId`. Memory and approved file context live under `<project folder>/EgomorphCore/model-home`; the bridge restricts reads and writes to documented file types and paths.

The UI uses the abstract `egomorph-core.svg` wordmark. Its CSS animation moves only the logo. Character, classification, feedback-traini
[truncated — 147 more characters]
```

### package.json

```
{
  "name": "egomorph",
  "version": "1.0.0",
  "description": "Egomorph Core: lokales Gateway fuer Dashboard, Codex-Bridge und agentische Browser-App.",
  "main": "index.html",
  "bin": {
    "egomorph": "egomorph"
  },
  "scripts": {
    "build:safetyfilter": "tsc",
    "dashboard": "node egomorph dashboard --no-open",
    "gateway": "node egomorph gateway",
    "codex:bridge": "node egomorph gateway",
    "codex:login": "node egomorph codex login",
    "codex:login:device": "node egomorph codex login --device-auth",
    "codex:login:persistent": "node egomorph codex login",
    "codex:login:persistent:device": "node egomorph codex login --device-auth",
    "codex:status": "node egomorph codex status",
    "pwa:validate": "node scripts/validate-pwa.js",
    "serve": "node egomorph dashboard --no-open",
    "test": "jest"
  },
  "author": "",
  "license": "MIT",
  "type": "commonjs",
  "devDependencies": {
    "jest": "^29.7.0",
    "typescript": "^5.9.3"
  }
}

```

### app.js

```javascript
(function () {
  'use strict';

  var locales = window.EgoMorphLocales || {};
  var language = safeGet('egoLanguage', 'de');
  if (!locales[language]) language = 'de';
  var translations = {};
  Object.keys(locales).forEach(function (key) { translations[key] = locales[key].ui || {}; });
  window.__egoTranslations = translations;
  window.__egoCurrentLanguage = language;
  window.egoT = function (key) {
    return translations[language] && translations[language][key] != null
      ? translations[language][key]
      : translations.de && translations.de[key] != null ? translations.de[key] : key;
  };

  var input = byId('inputText');
  var inputForm = byId('inputForm');
  var responseBox = byId('response');
  var sendButton = byId('sendBtn');
  var stopButton = byId('stopBtn');
  var speechButton = byId('speechButton');
  var activeController = null;
  var pendingMarkdownPaths = [];
  var voiceEnabled = safeGet('voiceEnabled', 'true') !== 'false';
  var threads = createConversationThreadsStore();
  var activeThreadId = threads.getActiveThread().id;
  var conversation = threads.getActiveConversation();

  function byId(id) { return document.getElementById(id); }
  function safeGet(key, fallback) {
    try { var value = localStorage.getItem(key); return value == null ? fallback : value; }
    catch (_) { return fallback; }
  }
  function safeSet(key, value) { try { localStorage.setItem(key, String(value)); } catch (_) {} }
  function escapeHtml(value) {
    return String(value == null ? '' : value).replace(/[&<>"']/g, function (char) {
      return { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[char];
    });
  }
  function format(key, fallback, values) {
    var value = window.egoT(key);
    if (!value || value === key) value = fallback;
    Object.keys(values || {}).forEach(function (name) {
      value = String(value).replace(new RegExp('\\{' + name + '\\}', 'g'), values[name]);
    });
    return value;
  }
  function createConversationThreadsStore() {
    if (window.EgoConversationStore && typeof window.EgoConversationStore.create === 'function') {
      return window.EgoConversationStore.create(localStorage);
    }
    console.error('[conversation] conversationStore.js fehlt; starte mit flüchtigem Notfall-Speicher.');
    var thread = { id: 'temporary', title: '', conversation: [] };
    return {
      getState: function () { return { activeThreadId: thread.id, threads: [thread] }; },
      getActiveThread: function () { return thread; },
      getActiveConversation: function () { return thread.conversation; },
      createThread: function () { thread = { id: 'temporary-' + Date.now(), title: '', conversation: [] }; return thread; },
      switchThread: function () { return thread; },
      setConversation: function (_, value) { thread.conversation = value.slice(); return thread; },
      clearThread: function () { thread.conversation = []; return thread; },
      deleteThread: function () { thread = { id: 'temporary-' + Date.now(), title: '', conversation: [] }; return thread; }
    };
  }

  function persistConversation() { threads.setConversation(activeThreadId, conversation); }
  function translatedSkillName(skillId) {
    if (skillId === 'codex.web_search') return window.egoT('codexWebSearchSkillName');
    var skill = window.EgoSkillSystem && window.EgoSkillSystem.getSkill
      ? window.EgoSkillSystem.getSkill(skillId)
      : null;
    var manifest = skill && skill.manifest;
    return manifest
      ? translatedManifestText(manifest.displayNameKey, manifest.name || skillId)
      : String(skillId || '');
  }
  function skillRunStatus(run) {
    if (run && run.status === 'blocked') return window.egoT('agentSkillBlocked');
    if (!run || run.status === 'running') return window.egoT('agentSkillRunning');
    if (run.status === 'failed') return window.egoT('agentSkillFailed');
    if (run.id === 'workspace.extended-files' && run.operation === 'read') return window.egoT('agentSkillCompletedRead');
    if (run.id === 'workspace.extended-files' && run.operation === 'write') return window.egoT('agentSkillCompletedWrite');
    if (run.resultCount === 0) return window.egoT('agentSkillCompletedNoSources');
    if (Number(run.resultCount) > 0) {
      return format('agentSkillCompletedSources', '{count} Quellen verwendet', { count: run.resultCount });
    }
    return window.egoT('agentSkillCompleted');
  }
  function renderAgentReply(turn) {
    var thought = String(turn.thought || window.egoT('agentThoughtFallback'));
    var skillRuns = Array.isArray(turn.skillRuns) ? turn.skillRuns : [];
    if (!skillRuns.length && Array.isArray(turn.skills)) {
      skillRuns = turn.skills.filter(Boolean).map(function (id) { return { id: id, status: 'completed' }; });
    }
    var html = '<section class="agent-step agent-thought"><strong>' + escapeHtml(window.egoT('agentThoughtLabel')) + '</strong><span>' + escapeHtml(thought) + '</span></section>';
    if (skillRuns.length) {
      html += '<section class="agent-step agent-skill"><strong>' + escapeHtml(window.egoT('agentSkillLabel')) + '</strong>' +
        '<div class="agent-skill-runs">' + skillRuns.map(function (run) {
          return '<span class="agent-skill-run" data-status="' + escapeHtml(run.status || 'completed') + '">' +
            escapeHtml(translatedSkillName(run.id)) + ' · ' + escapeHtml(skillRunStatus(run)) + '</span>';
        }).join('') + '</div></section>';
    }
    html += '<div class="agent-final-separator">' + escapeHtml(window.egoT('agentFinalLabel')) + '</div>' +
      '<div class="agent-final-answer">' + escapeHtml(turn.reply || (turn.pending ? window.egoT('agentFinalWaiting') : '')) + '</div>';
    return html;
  }
  function renderConversation() {
    if (!responseBox) return;
    responseBox.innerHTML = conversation.map(function (turn) {
      return '<article class="conversation-turn">' +
        '<div class="conversation-line conversation-user"><strong>' + escapeHtml(window.egoT('youPrefix
[truncated — 30526 more characters]
```

### loader.js

```javascript


(function() {
    // 1. Konfiguration
    const CONFIG = {
        title: "EGOMORPH",
        version: "2026-07-13 Beta",
        description: "Initialisiere Systemkerne...",
        duration: 1600,
        failsafe: 9000
    };


    const splashHTML = `
        <div class="ego-morph-container">
            <div class="ego-shape"></div>
        </div>
        <h1 class="ego-title">${CONFIG.title}</h1>
        <div class="ego-version">${CONFIG.version}</div>
        <p class="ego-desc">${CONFIG.description}</p>
        <div class="ego-loader">
            <div class="ego-progress"></div>
        </div>
    `;

    // 3. Das Container-Element erzeugen
    if (document.getElementById('egomorph-overlay')) return;
    const overlay = document.createElement('div');
    overlay.id = 'egomorph-overlay';
    overlay.innerHTML = splashHTML;


    document.body.prepend(overlay);
    
    // Damit man während des Ladens nicht scrollen kann
    const originalOverflow = document.body.style.overflow;
    document.body.style.overflow = 'hidden';


    const startedAt = Date.now();
    let dismissScheduled = false;
    function dismissOverlay() {
        if (dismissScheduled) return;
        dismissScheduled = true;
        const remaining = Math.max(0, CONFIG.duration - (Date.now() - startedAt));
        setTimeout(() => {
            overlay.classList.add('ego-hidden');
            document.body.style.overflow = originalOverflow;
            setTimeout(() => {
                if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
            }, 800);
        }, remaining);
    }

    if (document.readyState === 'complete') {
        dismissOverlay();
    } else {
        window.addEventListener('load', dismissOverlay, { once: true });
    }
    // A hanging third-party resource must not trap the user behind the splash.
    setTimeout(dismissOverlay, CONFIG.failsafe);

})();

```

### load-screen.css

```css

#egomorph-overlay {
    position: fixed;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    background-color: #000000;
    z-index: 2147483647; /* Maximaler Z-Index, damit es ÜBERALLEM liegt */
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-family: 'Courier New', Courier, monospace; /* Fallback Font */
    color: white;
    transition: opacity 0.8s ease-out;
}

/* Der Morphing-Effekt */
.ego-morph-container {
    position: relative;
    width: 120px;
    height: 120px;
    margin-bottom: 2rem;
}

.ego-shape {
    position: absolute;
    inset: 0;
    background: linear-gradient(135deg, #6d28d9, #2563eb);
    border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%;
    animation: ego-morph 6s ease-in-out infinite;
    box-shadow: 0 0 40px rgba(109, 40, 217, 0.5);
}

/* Text Styling */
.ego-title {
    font-size: 2.5rem;
    font-weight: 800;
    letter-spacing: 0.3rem;
    text-transform: uppercase;
    background: linear-gradient(to right, #fff, #a5b4fc);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    margin: 0;
    opacity: 0;
    animation: ego-fadeUp 0.8s forwards 0.3s;
}

.ego-version {
    font-size: 0.75rem;
    color: #6b7280;
    margin-top: 0.5rem;
    border: 1px solid #374151;
    padding: 2px 8px;
    border-radius: 4px;
    opacity: 0;
    animation: ego-fadeUp 0.8s forwards 0.5s;
}

.ego-desc {
    color: #9ca3af;
    font-size: 0.9rem;
    margin-top: 1rem;
    opacity: 0;
    animation: ego-fadeUp 0.8s forwards 0.7s;
}

/* Ladebalken */
.ego-loader {
    width: 150px;
    height: 2px;
    background: #1f2937;
    margin-top: 2rem;
    border-radius: 2px;
    overflow: hidden;
    opacity: 0;
    animation: ego-fadeIn 1s forwards 1s;
}

.ego-progress {
    height: 100%;
    width: 0%;
    background: #8b5cf6;
    animation: ego-load 3.5s ease-in-out forwards;
}

/* Animation Keyframes */
@keyframes ego-morph {
    0% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; }
    50% { border-radius: 30% 60% 70% 40% / 50% 60% 30% 60%; transform: rotate(180deg); }
    100% { border-radius: 60% 40% 30% 70% / 60% 30% 70% 40%; transform: rotate(360deg); }
}

@keyframes ego-fadeUp {
    from { opacity: 0; transform: translateY(10px); }
    to { opacity: 1; transform: translateY(0); }
}

@keyframes ego-fadeIn { to { opacity: 1; } }

@keyframes ego-load {
    0% { width: 0%; }
    100% { width: 100%; }
}

/* Klasse zum Ausblenden */
.ego-hidden {
    opacity: 0;
    pointer-events: none;
}


```

### sw.js

```javascript
// Service Worker fuer App-Shell und Gateway-Dashboard: cached statische
// UI-Dateien, laesst Gateway-API-Routen aber immer am Cache vorbei.
const CACHE_NAME = 'egomorph-core-v38';
const URLS_TO_CACHE = [
  './',
  'index.html',
  'manifest.json',
  'style.css',
  'load-screen.css',
  'loader.js',
  'skills/internetSkill.js',
  'skills/internet/manifest.json',
  'skills/extendedFileSkill.js',
  'skills/extended-files/manifest.json',
  'skills/learnWithEgomorphSkill.js',
  'skills/learn-with-egomorph/manifest.json',
  'skillSystem.js',
  'agentResponse.js',
  'conversationStore.js',
  'resourceProfile.js',
  'app.js',
  'Safetyfilter.js',
  'chatModel.js',
  'ltmManager.js',
  'translations/de.js',
  'translations/en.js',
  'translations/fr.js',
  'Writer.js',
  'ego_icon_192.png',
  'ego_icon_512.png',
  'egomorph-core.svg'
];

function isGatewayApiPath(pathname) {
  return pathname === '/health' ||
    pathname === '/gateway/status' ||
    pathname === '/codex/status' ||
    pathname.startsWith('/codex/') ||
    pathname === '/v1/models' ||
    pathname === '/v1/chat/completions' ||
    pathname.startsWith('/v1/') ||
    pathname.startsWith('/egomorph/');
}

self.addEventListener('install', event => {
  event.waitUntil(
    self.registration.active
      ? Promise.resolve()
      : caches.open(CACHE_NAME)
        .then(cache => cache.addAll(URLS_TO_CACHE))
        .then(() => self.skipWaiting())
  );
});

self.addEventListener('message', event => {
  if (!event.data || event.data.type !== 'DOWNLOAD_UPDATE') return;

  event.waitUntil(
    caches.open(CACHE_NAME)
      .then(cache => cache.addAll(URLS_TO_CACHE))
      .then(() => self.skipWaiting())
  );
});

self.addEventListener('activate', event => {
  event.waitUntil(
    caches.keys()
      .then(keys => Promise.all(
        keys
          .filter(key => key !== CACHE_NAME)
          .map(key => caches.delete(key))
      ))
      .then(() => self.clients.claim())
  );
});

self.addEventListener('fetch', event => {
  if (event.request.method !== 'GET') return;

  const requestUrl = new URL(event.request.url);
  if (requestUrl.origin !== self.location.origin) {
    event.respondWith(fetch(event.request).catch(() => Response.error()));
    return;
  }

  if (isGatewayApiPath(requestUrl.pathname)) {
    event.respondWith(fetch(event.request).catch(() => Response.error()));
    return;
  }

  if (event.request.mode === 'navigate') {
    event.respondWith(
      fetch(event.request).catch(() => caches.match('index.html'))
    );
    return;
  }

  event.respondWith(
    caches.match(event.request)
      .then(response => response || fetch(event.request).then(networkResponse => {
        const copy = networkResponse.clone();
        caches.open(CACHE_NAME).then(cache => cache.put(event.request, copy));
        return networkResponse;
      }).catch(() => Response.error()))
  );
});

```

### agentResponse.js

```javascript
(function (root, factory) {
  var api = factory();
  if (typeof module === 'object' && module.exports) module.exports = api;
  if (root) root.EgoAgentResponse = api;
})(typeof window !== 'undefined' ? window : globalThis, function () {
  'use strict';

  var THOUGHT_TAG = 'egomorph_thought';
  var FINAL_TAG = 'egomorph_final';
  var INTERNAL_REFERENCE = '[interner Dateiverweis ausgeblendet]';

  function escapeRegex(value) {
    return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  function sanitize(text, protectedPaths) {
    var value = String(text == null ? '' : text);
    var paths = Array.isArray(protectedPaths) ? protectedPaths : [];
    for (var i = 0; i < paths.length; i++) {
      var path = String(paths[i] || '').trim();
      if (!path) continue;
      value = value.replace(new RegExp(escapeRegex(path), 'gi'), INTERNAL_REFERENCE);
    }
    value = value
      .replace(/(?:[a-z]:[\\/]|\/)?(?:[^\s"'<>]+[\\/])*EgomorphCore[\\/]model-home(?:[\\/][^\s"'<>]*)?/gi, INTERNAL_REFERENCE)
      .replace(/\bmemory\.md\b/gi, INTERNAL_REFERENCE)
      .replace(/\b(?:Egomorph Core Modell-Home-Kontext|Persistenter Egomorph-Core-Nutzerkontext|Inhalt von memory\.md)\s*:?/gi, INTERNAL_REFERENCE);
    return value.replace(/(?:\[interner Dateiverweis ausgeblendet\]\s*){2,}/g, INTERNAL_REFERENCE + ' ').trim();
  }

  function taggedValue(text, tag) {
    var match = String(text || '').match(new RegExp('<' + tag + '>([\\s\\S]*?)<\\/' + tag + '>', 'i'));
    return match ? match[1].trim() : '';
  }

  function stripIncompleteTag(text) {
    return String(text || '').replace(/<[^>]*$/, '');
  }

  function parse(rawReply, options) {
    var opts = options || {};
    var raw = String(rawReply == null ? '' : rawReply).trim();
    var thought = taggedValue(raw, THOUGHT_TAG);
    var finalReply = taggedValue(raw, FINAL_TAG);
    if (!finalReply) finalReply = raw;
    if (!thought) thought = String(opts.fallbackThought || '').trim();
    return {
      thought: sanitize(thought, opts.protectedPaths),
      reply: sanitize(finalReply, opts.protectedPaths),
      skills: Array.isArray(opts.skills)
        ? opts.skills.map(function (skill) { return String(skill || '').trim(); }).filter(Boolean)
        : []
    };
  }

  function parseLive(rawReply, options) {
    var opts = options || {};
    var raw = String(rawReply == null ? '' : rawReply);
    var thoughtOpen = raw.toLowerCase().indexOf('<' + THOUGHT_TAG + '>');
    var thoughtClose = raw.toLowerCase().indexOf('</' + THOUGHT_TAG + '>');
    var finalOpen = raw.toLowerCase().indexOf('<' + FINAL_TAG + '>');
    var finalClose = raw.toLowerCase().indexOf('</' + FINAL_TAG + '>');
    var thought = '';
    var finalReply = '';

    if (thoughtOpen !== -1) {
      var thoughtStart = thoughtOpen + THOUGHT_TAG.length + 2;
      thought = stripIncompleteTag(raw.slice(thoughtStart, thoughtClose === -1 ? raw.length : thoughtClose));
    }
    if (finalOpen !== -1) {
      var finalStart = finalOpen + FINAL_TAG.length + 2;
      finalReply = stripIncompleteTag(raw.slice(finalStart, finalClose === -1 ? raw.length : finalClose));
    } else if (thoughtOpen === -1 && raw.indexOf('<') !== 0) {
      finalReply = raw;
    }

    return {
      thought: sanitize(thought || opts.fallbackThought || '', opts.protectedPaths),
      reply: sanitize(finalReply, opts.protectedPaths)
    };
  }

  return {
    THOUGHT_TAG: THOUGHT_TAG,
    FINAL_TAG: FINAL_TAG,
    INTERNAL_REFERENCE: INTERNAL_REFERENCE,
    parse: parse,
    parseLive: parseLive,
    sanitize: sanitize
  };
});

```

### ltmManager.js

```javascript
(function() {
  // Lightweight long-term memory manager
  const LTM_KEY = 'egoLongTermMemory';
  const MAX = 200;

  function hasStorage() {
    return typeof localStorage !== 'undefined' &&
      localStorage &&
      typeof localStorage.getItem === 'function' &&
      typeof localStorage.setItem === 'function';
  }

  function sanitiseEntry(entry) {
    if (!entry) return null;
    if (typeof entry === 'string') {
      return { text: String(entry).slice(0, 280), topics: [], ts: 0, hits: 0 };
    }
    if (typeof entry !== 'object') return null;
    const text = entry.text == null ? '' : String(entry.text).slice(0, 280);
    const hits = typeof entry.hits === 'number' && Number.isFinite(entry.hits) ? entry.hits : 0;
    return {
      ...entry,
      text,
      topics: normaliseTopics(entry.topics),
      ts: normaliseTimestamp(entry.ts),
      hits
    };
  }
  function loadLTM() {
    if (!hasStorage()) return [];
    try {
      const raw = localStorage.getItem(LTM_KEY);
      if (!raw) return [];
      const parsed = JSON.parse(raw);
      if (!Array.isArray(parsed)) return [];
      const normalised = [];
      for (const entry of parsed) {
        const sanitised = sanitiseEntry(entry);
        if (sanitised) normalised.push(sanitised);
      }
      return normalised;
    } catch (e) {
      return [];
    }
  }

  function saveLTM(arr) {
    if (!Array.isArray(arr)) return;
    if (!hasStorage()) return;
    try { localStorage.setItem(LTM_KEY, JSON.stringify(arr)); } catch (e) {}
  }

  function normaliseTimestamp(ts) {
    if (typeof ts === 'number' && Number.isFinite(ts)) return ts;
    return 0;
  }
  function addLongTermMemory(entry) {
    if (!entry || !entry.text) return;
    const now = Date.now();
    const ltm = loadLTM();
    const text = String(entry.text).trim().slice(0, 280);
    if (!text) return;
    const topics = normaliseTopics(entry.topics);
    const existing = ltm.find(item => String(item.text || '').trim().toLowerCase() === text.toLowerCase());
    if (existing) {
      existing.ts = Math.max(normaliseTimestamp(existing.ts), now);
      existing.topics = mergeTopics(existing.topics, topics);
    } else {
      ltm.push({ text, topics, ts: now, hits: 0 });
    }
    ltm.sort((a, b) => {
      const hitDelta = (b.hits || 0) - (a.hits || 0);
      if (hitDelta !== 0) return hitDelta;
      const tsA = normaliseTimestamp(a.ts);
      const tsB = normaliseTimestamp(b.ts);
      return tsB - tsA;
    });
    const trimmed = ltm.slice(0, MAX);
    saveLTM(trimmed);
  }

  function exportLongTermMemory() {
    const data = loadLTM();
    const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = 'egomorph_ltm.json';
    a.click();
    URL.revokeObjectURL(url);
  }

  function clearLongTermMemory() {
    saveLTM([]);
  }

  function normaliseTopics(topics) {
    if (Array.isArray(topics)) return topics;
    if (topics == null) return [];
    return [topics];
  }

  function mergeTopics(a, b) {
    const seen = new Set();
    const merged = [];
    normaliseTopics(a).concat(normaliseTopics(b)).forEach(topic => {
      const text = String(topic == null ? '' : topic).trim();
      const key = text.toLowerCase();
      if (!text || seen.has(key)) return;
      seen.add(key);
      merged.push(text);
    });
    return merged.slice(-20);
  }

  function queryLongTermMemory(query, k = 3) {
    const ltm = loadLTM();
    const q = String(query == null ? '' : query).trim().toLowerCase();
    if (!q) return [];
    for (const e of ltm) {
      const txt = String(e.text == null ? '' : e.text).toLowerCase();
      const topicMatch = normaliseTopics(e.topics).some(t => String(t).toLowerCase().includes(q));
      const has = txt.includes(q) ? 1 : 0;
      const matchScore = has + (topicMatch ? 1 : 0);
      if (matchScore === 0) {
        e._score = 0;
        continue;
      }
      const ageDays = Math.max(0, (Date.now() - (e.ts || 0)) / (1000 * 60 * 60 * 24));
      const recencyBoost = 1 / (1 + ageDays);
      const hitScore = Math.log10((Math.max(0, e.hits || 0)) + 1);
      e._score = matchScore + hitScore + recencyBoost;
    }
    ltm.sort((a, b) => (b._score || 0) - (a._score || 0));
    const top = ltm.filter(e => (e._score || 0) > 0).slice(0, k);
    for (const e of top) {
      e.hits = (e.hits || 0) + 1;
      delete e._score;
    }
    for (const e of ltm) {
      if (e._score != null) delete e._score;
    }
    saveLTM(ltm);
    return top.map(e => e.text);
  }

  // Hook into memory persistence spot to also capture LTM
  if (hasStorage() && !localStorage.__egoLtmHooked) {
    const _setItem = localStorage.setItem.bind(localStorage);
    localStorage.setItem = function(key, value) {
      try {
        if (key === 'egoConversation') {
          const conv = JSON.parse(value);
          const topics = JSON.parse(localStorage.getItem('egoMemoryTopics') || '[]');
          if (Array.isArray(conv) && conv.length > 0) {
            for (let i = conv.length - 1; i >= 0; i--) {
              if (conv[i] && conv[i].user && String(conv[i].user).trim()) {
                addLongTermMemory({ text: conv[i].user, topics });
                break;
              }
            }
          }
        } else if (key === 'egoMemory') {
          const mem = JSON.parse(value);
          const topics = JSON.parse(localStorage.getItem('egoMemoryTopics') || '[]');
          if (Array.isArray(mem) && mem.length > 0) {
            addLongTermMemory({ text: mem[mem.length - 1], topics });
          }
        }
      } catch (_) {}
      return _setItem(key, value);
    };
    try {
      Object.defineProperty(localStorage, '__egoLtmHooked', { value: true, configurable: true });
    } catch (_) {
      localStorage.__egoLtmHooked = true;
    }
  }

  if (typeof window !== 'undefined') {
    window.exportLongTermMemory = exportLongTermMemory;
   
[truncated — 115 more characters]
```

### Safetyfilter.js

```javascript
"use strict";
/**
 * Safetyfilter.ts – Sicherheitsfilter für anstößige Modell-Ausgaben.
 *
 * Wird im "Full"-Modus (lokales LLM via Transformers.js) auf die generierte
 * Antwort angewendet, bevor sie der Nutzer:in angezeigt wird. In anderen
 * Profilen (api, codex) bleibt der Filter inaktiv – er kann dort
 * aber jederzeit explizit aufgerufen werden.
 *
 * Externe Einbindung über <script src="Safetyfilter.js"></script> nach
 * Kompilierung mit `tsc`. Der Filter registriert sich global als
 * `window.SafetyFilter` und wird von chatModel.js automatisch genutzt,
 * sofern verfügbar.
 */
(function () {
    'use strict';
    // Wortliste für Deutsch + Englisch. Bewusst klein gehalten und auf klar
    // anstößige Begriffe (Beleidigungen, Gewalt, Sexualisierung, Hass)
    // beschränkt. Keine Stoppwort-Listen oder politische Begriffe.
    const BLOCKED_TERMS = [
        // Beleidigungen / Hass (DE)
        'arschloch', 'arschlöcher', 'wichser', 'fotze', 'fotzen', 'hure', 'huren',
        'hurensohn', 'hurensöhne', 'schlampe', 'schlampen', 'missgeburt',
        'missgeburten', 'spast', 'spasti', 'spastiker', 'mongo', 'mongos',
        'krüppel', 'behindi', 'kanake', 'kanaken', 'nigger', 'neger',
        'judensau', 'untermensch', 'untermenschen',
        // Gewalt / Drohung (DE)
        'umbringen', 'töten', 'erschießen', 'erstechen', 'vergewaltigen',
        'vergewaltigung', 'abschlachten', 'massakrieren',
        // Sexualisierung / explizit (DE)
        'kinderporno', 'kinderpornos', 'kinderpornographie', 'kinderpornografie',
        'pädophil', 'paedophil', 'pädo', 'paedo',
        // Beleidigungen / Hass (EN)
        'asshole', 'assholes', 'bitch', 'bitches', 'cunt', 'cunts', 'whore',
        'whores', 'slut', 'sluts', 'faggot', 'faggots', 'retard', 'retards',
        'nigga', 'niggas',
        // Gewalt / Drohung (EN)
        'kill yourself', 'kys', 'rape', 'raping', 'molest', 'molesting',
        // Sexualisierung / explizit (EN)
        'child porn', 'childporn', 'cp ', 'pedo', 'pedophile',
    ];
    const DEFAULT_BLOCK_RESPONSE = 'Entschuldigung, diese Antwort wurde aus Sicherheitsgründen gefiltert.';
    function escapeRegex(s) {
        return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }
    function buildPattern(terms) {
        // Phrasen (mit Leerzeichen) und Einzelwörter werden gemeinsam erkannt.
        // Das führende Trennzeichen wird als Gruppe erfasst, damit kein Regex-
        // Lookbehind nötig ist und der Filter auch in älteren Browsern läuft.
        const escaped = terms
            .slice()
            .sort(function (a, b) { return b.length - a.length; })
            .map(escapeRegex);
        return new RegExp('(^|[^\\p{L}])(' + escaped.join('|') + ')(?![\\p{L}])', 'giu');
    }
    function normalize(text) {
        return (text || '').toLowerCase();
    }
    function dedupe(values) {
        const seen = {};
        const out = [];
        for (let i = 0; i < values.length; i++) {
            const v = values[i];
            if (!seen[v]) {
                seen[v] = true;
                out.push(v);
            }
        }
        return out;
    }
    function getActiveTerms(extra) {
        if (!extra || extra.length === 0)
            return BLOCKED_TERMS;
        const merged = BLOCKED_TERMS.slice();
        for (let i = 0; i < extra.length; i++) {
            const t = (extra[i] || '').toLowerCase().trim();
            if (t)
                merged.push(t);
        }
        return dedupe(merged);
    }
    function contains(text, extraTerms) {
        if (!text || typeof text !== 'string')
            return false;
        const pattern = buildPattern(getActiveTerms(extraTerms));
        return pattern.test(normalize(text));
    }
    function filter(text, options) {
        const opts = options || {};
        const maskChar = opts.maskChar && opts.maskChar.length > 0 ? opts.maskChar[0] : '*';
        const blockOnMatch = opts.blockOnMatch === true;
        if (!text || typeof text !== 'string') {
            return { text: text || null, flagged: false, matches: [] };
        }
        const pattern = buildPattern(getActiveTerms(opts.extraTerms));
        const found = [];
        const cleaned = text.replace(pattern, function (match, prefix, term) {
            found.push(term.toLowerCase());
            return prefix + maskChar.repeat(term.length);
        });
        const flagged = found.length > 0;
        const matches = dedupe(found);
        if (flagged && blockOnMatch) {
            return { text: null, flagged: true, matches: matches };
        }
        return { text: cleaned, flagged: flagged, matches: matches };
    }
    /**
     * Convenience-Wrapper, der von chatModel.js im Full-Modus aufgerufen wird.
     * Bei eindeutig anstößigem Inhalt wird die Antwort komplett ersetzt,
     * damit nicht nur ein zerlöcherter Satz übrig bleibt.
     */
    function filterModelOutput(text) {
        if (text === null || text === undefined)
            return null;
        if (typeof text !== 'string' || text.length === 0)
            return null;
        const result = filter(text, { blockOnMatch: false });
        if (!result.flagged)
            return result.text;
        if (result.matches.length >= 2 || result.text === null) {
            try {
                if (typeof console !== 'undefined' && console.warn) {
                    console.warn('[SafetyFilter] Antwort blockiert. Treffer:', result.matches);
                }
            }
            catch (_) { /* ignore */ }
            return DEFAULT_BLOCK_RESPONSE;
        }
        try {
            if (typeof console !== 'undefined' && console.warn) {
                console.warn('[SafetyFilter] Antwort maskiert. Treffer:', result.matches);
            }
        }
        catch (_) { /* ignore */ }
        return result.text;
    }
    function getBlockedTerms() {
        return BLOCKED_TERMS.slice();
    }
    const api = {
        contains: contains,
        filter: filter,
        fil
[truncated — 445 more characters]
```

[44 more indexed source files omitted to keep this export small. The full file list is in the Codebase structure section above.]