Full Code of dtinth/automatron for AI

main 00720cfa1c6a cached
74 files
523.8 KB
226.5k tokens
167 symbols
1 requests
Download .txt
Showing preview only (554K chars total). Download the full file or copy to clipboard to get everything.
Repository: dtinth/automatron
Branch: main
Commit: 00720cfa1c6a
Files: 74
Total size: 523.8 KB

Directory structure:
gitextract_gdn6le0m/

├── .devcontainer/
│   ├── devcontainer.json
│   └── postbuild
├── .gitattributes
├── .gitignore
├── .prettierrc
├── .vscode/
│   ├── settings.json
│   └── tasks.json
├── README.md
├── core/
│   ├── README.md
│   ├── package.json
│   ├── src/
│   │   ├── BotSecrets.ts
│   │   ├── CodeEvaluation.ts
│   │   ├── Cron.ts
│   │   ├── DataEncryption.ts
│   │   ├── DeviceTracking.ts
│   │   ├── ExpenseTracking.ts
│   │   ├── ExpenseTrackingGrist.ts
│   │   ├── HomeAutomation.ts
│   │   ├── ImageMessageHandler.ts
│   │   ├── LINEClient.ts
│   │   ├── LINEMessageUtilities.ts
│   │   ├── LanguageModelAssistant.ts
│   │   ├── MessageHandler.ts
│   │   ├── MessageHistory.ts
│   │   ├── MongoDatabase.ts
│   │   ├── NotificationProcessor.ts
│   │   ├── PersistentState.ts
│   │   ├── PhoneFinder.ts
│   │   ├── PreludeCode.ts
│   │   ├── RomanNumerals.ts
│   │   ├── SMSHandler.ts
│   │   ├── SlackMessageUtilities.ts
│   │   ├── SpeedDial.ts
│   │   ├── SpendingTracking.ts
│   │   ├── TemporaryBlobStorage.ts
│   │   ├── Tracing.ts
│   │   ├── TypedGristDocAPI.ts
│   │   ├── bot.ts
│   │   ├── logger.ts
│   │   ├── modules.d.ts
│   │   ├── scripts/
│   │   │   └── updateEnv.ts
│   │   ├── server.ts
│   │   ├── typedefs.d.ts
│   │   └── types.ts
│   └── tsconfig.json
├── fnox.toml
├── frpc.toml
├── images/
│   └── .gitkeep
├── mise.toml
├── package.json
├── pnpm-workspace.yaml
└── webui/
    ├── .eslintrc.cjs
    ├── .gitignore
    ├── README.md
    ├── app/
    │   ├── Clock.tsx
    │   ├── backend.ts
    │   ├── firebase.ts
    │   ├── index.css
    │   ├── requireAuth.ts
    │   ├── root.tsx
    │   └── routes/
    │       ├── _index.tsx
    │       ├── automatron._index.tsx
    │       ├── automatron.knobs.tsx
    │       └── automatron.tsx
    ├── env.d.ts
    ├── package.json
    ├── playwright-report/
    │   └── index.html
    ├── playwright.config.ts
    ├── postcss.config.cjs
    ├── tailwind.config.cjs
    ├── tests/
    │   └── automatron.spec.ts
    ├── tsconfig.json
    ├── vercel.json
    └── vite.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .devcontainer/devcontainer.json
================================================
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.192.0/containers/codespaces-linux
{
  "image": "mcr.microsoft.com/devcontainers/universal:2",
  "containerEnv": {
    "GOOGLE_APPLICATION_CREDENTIALS": "/home/codespace/.google-cloud-service-account.json"
  },
  "postCreateCommand": "pnpm install && ./.devcontainer/postbuild"
}


================================================
FILE: .devcontainer/postbuild
================================================
#!/bin/bash -e
node core/dev set-up-codespaces

================================================
FILE: .gitattributes
================================================
# Don't allow people to merge changes to these generated files, because the result
# may be invalid.  You need to run "rush update" again.
pnpm-lock.yaml               merge=binary
shrinkwrap.yaml              merge=binary
npm-shrinkwrap.json          merge=binary
yarn.lock                    merge=binary

# Rush's JSON config files use JavaScript-style code comments.  The rule below prevents pedantic
# syntax highlighters such as GitHub's from highlighting these comments as errors.  Your text editor
# may also require a special configuration to allow comments in JSON.
#
# For more information, see this issue: https://github.com/microsoft/rushstack/issues/1088
#
*.json                       linguist-language=JSON-with-Comments


================================================
FILE: .gitignore
================================================
.env
.cache
node_modules
/automatron.js
/automatron.js.map
/dist/
*.gz
webpack.stats.json
*.env

# Rush temporary files
common/deploy/
common/temp/
common/autoinstallers/*/.npmrc
**/.rush/temp/

# Heft
.heft


================================================
FILE: .prettierrc
================================================
{
  "singleQuote": true,
  "semi": false
}


================================================
FILE: .vscode/settings.json
================================================
{
  "cSpell.words": ["Automatron"],
  "typescript.experimental.useTsgo": true
}


================================================
FILE: .vscode/tasks.json
================================================
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "pnpm: dev",
      "type": "shell",
      "command": "pnpm dev",
      "group": "build"
    },
    {
      "label": "pnpm: tunnel",
      "type": "shell",
      "command": "pnpm tunnel",
      "group": "build"
    },
    {
      "label": "pnpm: dev+tunnel",
      "type": "shell",
      "command": "echo Running pnpm dev and pnpm tunnel in parallel...",
      "dependsOn": ["pnpm: dev", "pnpm: tunnel"],
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "problemMatcher": []
    }
  ]
}


================================================
FILE: README.md
================================================
# automatron

This is my personal LINE bot that helps me automate various tasks of everyday life, such as
**home control** (air conditioner, lights and plugs) and **expense tracking** (record how much I spend each day).
[See below for a feature tour.](#features)

I recommend every developer to try creating their own personal assistant chat bot.
It’s a great way to practice coding and improve problem solving skills.
And it helps make life more convenient!

It is written in TypeScript and runs on [DigitalOcean App Platform](https://www.digitalocean.com/products/app-platform) with Node.js 24.

## features

- [home automation](#home-automation)
- [expense tracking](#expense-tracking)
- [transaction aggregation](#transaction-aggregation)
- [image-to-text](#image-to-text)
- [livescript evaluation](#livescript-evaluation)

### home automation

![home automation](./images/home_automation.png)

I have a [Home Assistant](https://www.home-assistant.io/) setup which can [control lights](https://www.home-assistant.io/integrations/hue/), [air conditioner](https://www.home-assistant.io/integrations/broadlink/#remote), and [smart plugs](https://www.home-assistant.io/integrations/tplink/). automatron can send commands to [Home Assistant’s REST API](https://developers.home-assistant.io/docs/api/rest/) to control these devices.

### expense tracking

![expense tracking](./images/expense_tracking.png)

Simple expense tracking by typing in the amount + category. Example: 50f means ฿50 for food. Data is saved in [Grist](https://www.getgrist.com/).

On mobile, tapping the bubble’s body (containing the amount) will take me to the created Grist record. This allows me to easily edit or add remarks to the record.

### transaction aggregation

![transaction_aggregation](./images/transaction_aggregation.png)

I built a [notification exfiltrator](https://docs.dt.in.th/dtinth.tools-android/exfiltrate.html) that send notifications my phone receive to automatron, which saves it to a database for later processing.

### image-to-text

![image_to_text](./images/image_to_text.png)

automatron can also convert image to text using [Google Cloud Vision API](https://cloud.google.com/vision/).

### livescript evaluation

![livescript](./images/livescript.png)

[LiveScript](https://livescript.net/) interpreter is included, which allows me to do some quick calculations.

### cli / api

![api](./images/api.png)

`POST /text` sends a text command to automatron. This is equivalent to sending a text message through LINE. This allows me to create a CLI tool that lets me talk to automatron from my terminal.

`POST /post` sends a message to my LINE account directly. This allows the [home automation](#home-automation) scripts to report back to me whenever the script is invoked.

`POST /run/automatron/reload` reloads the encrypted environment file (requires `X-API-Key` header with `API_KEY`).

## project structure

- [core](./core) — The core automatron service.
- [webui](./webui) — The web-based UI running on Vercel.

### other projects

- [automatron-prelude](https://github.dev/dtinth/automatron-prelude/blob/main/prelude.js) contains code experimentation.

### development setup

```sh
# Run the development server
pnpm dev

# Set up a tunnel using frp
pnpm tunnel

# Update a secret
pnpm env:set SECRET_NAME
```


================================================
FILE: core/README.md
================================================
# @dtinth/automatron-core

The core service of automatron. Provides:

- Chat bot interface (LINE, Slack)
- REST API interface
- Cron jobs

## secrets

The secret data required to run the automation are defined in [BotSecrets.ts](./src/BotSecrets.ts).

## development workflow

### developing

Watches for file changes and deploy the compiled code. Since it is my personal bot (I am the only one using it), I want a save-and-deploy workflow; there is no dev/staging environment at all.

```sh
node dev
```

### configuration

```sh
# download
gsutil cp gs://$GOOGLE_CLOUD_PROJECT-evalaas/evalaas/automatron.env automatron.env

# upload
gsutil cp automatron.env gs://$GOOGLE_CLOUD_PROJECT-evalaas/evalaas/automatron.env
```


================================================
FILE: core/package.json
================================================
{
  "name": "@dtinth/automatron-core",
  "private": true,
  "description": "My LINE bot!",
  "version": "1.0.0",
  "license": "Apache-2.0",
  "author": "Google Inc.",
  "engines": {
    "node": ">=24"
  },
  "repository": "https://github.com/dtinth/automatron",
  "main": "app.js",
  "scripts": {
    "start": "tsx src/server.ts",
    "dev": "env AUTOMATRON_ENV=dev tsx --watch src/server.ts"
  },
  "dependencies": {
    "age-encryption": "0.2.4",
    "google-application-credentials-base64": "0.0.0",
    "@dtinth/encrypted": "^0.3.1",
    "@google-cloud/firestore": "^5.0.2",
    "@google-cloud/storage": "^7.18.0",
    "@google-cloud/trace-agent": "^5.1.6",
    "@google-cloud/vision": "^2.4.2",
    "@line/bot-sdk": "^9.2.2",
    "@slack/types": "^1.10.0",
    "@types/cors": "~2.8.13",
    "airtable": "0.5.9",
    "axios": "0.18.0",
    "body-parser": "1.18.3",
    "cors": "~2.8.5",
    "citty": "0.1.6",
    "dotenv": "^7.0.0",
    "express": "4.16.4",
    "express-async-handler": "~1.2.0",
    "express-oauth2-jwt-bearer": "~1.1.0",
    "form-data": "~4.0.0",
    "google-auth-library": "~7.12.0",
    "jsonwebtoken": "^8.5.1",
    "lib": "~4.3.3",
    "livescript": "1.6.0",
    "lodash": "^4.17.21",
    "mongodb": "^4.13.0",
    "mqtt": "2.18.8",
    "nanoid": "~3.3.4",
    "pino": "^7.11.0",
    "prelude-ls": "1.1.2",
    "transaction-parser-th": "0.1.1",
    "tweetnacl": "^1.0.3",
    "tweetnacl-sealedbox-js": "^1.2.0",
    "verror": "^1.10.1",
    "tsx": "^4.21.0",
    "grist-api": "0.1.7",
    "@inquirer/prompts": "^8.0.2"
  },
  "devDependencies": {
    "@types/express": "^4.17.15",
    "@types/jsonwebtoken": "^8.5.9",
    "@types/node": "^24.10.1",
    "@vercel/ncc": "^0.36.0",
    "execa": "^1.0.0",
    "ora": "^3.4.0",
    "typescript": "^5.9.3",
    "yargs": "^13.3.2"
  }
}


================================================
FILE: core/src/BotSecrets.ts
================================================
export interface BotSecrets {
  /** The secret key that must be sent with the request to use the [API](../README.md#cli-api) */
  API_KEY: string
  /** Self-explanatory */
  LINE_CHANNEL_SECRET: string
  /** For development */
  DEV_LINE_CHANNEL_SECRET: string
  /** Self-explanatory */
  LINE_CHANNEL_ACCESS_TOKEN: string
  /** For development */
  DEV_LINE_CHANNEL_ACCESS_TOKEN: string
  /** My user ID, so that the bot receives commands from me only */
  LINE_USER_ID: string
  /** Self-explanatory @deprecated */
  AIRTABLE_API_KEY: string
  /** The ID of the Airtable base used for tracking expenses (should start with ‘app’) @deprecated */
  AIRTABLE_EXPENSE_BASE: string
  /** The web URL to the Airtable base, used for deep linking @deprecated */
  AIRTABLE_EXPENSE_URI: string
  /** The ID of the Airtable base used for cron jobs (should start with ‘app’) @deprecated */
  AIRTABLE_CRON_BASE: string
  /** Two numbers in form of A/B, where A is usage budget per day (rolls over to the next day) and B is starting budget */
  EXPENSE_PACEMAKER: string
  /** Slack webhook URL */
  SLACK_WEBHOOK_URL: string
  /** My user ID, so that the bot receives commands from me only */
  SLACK_USER_ID: string
  /** Google Cloud IoT Core device path to send */
  CLOUD_IOT_CORE_DEVICE_PATH: string
  /** GitHub OAuth App credentials for making unauthenticated API calls with elevated rate limits. In form of "<client_id>:<client_secret>" */
  GITHUB_OAUTH_APP_CREDENTIALS: string
  /** Encryption secret for decrypting */
  ENCRYPTION_SECRET: string
  /** Database storage */
  MONGODB_URL: string
  /** Subject ID for Google Auth */
  GOOGLE_AUTH_SUB: string

  /** Base URL for Grist */
  GRIST_BASE_URL: string
  /** API key for Grist */
  GRIST_API_KEY: string
  /** Document ID for Grist expense tracking */
  GRIST_EXPENSE_DOC_ID: string
  /** Document URI for Grist expense tracking */
  GRIST_EXPENSE_URI: string
}


================================================
FILE: core/src/CodeEvaluation.ts
================================================
import { getCodeExecutionContext } from './PreludeCode'
import { AutomatronContext, TextMessageHandler } from './types'

export async function evaluateCode(input: string, context: AutomatronContext) {
  const code = input.startsWith(';')
    ? input
    : require('livescript')
        .compile(input, {
          run: true,
          print: true,
          header: false,
        })
        .replace(/^\(function/, '(async function')
  console.log('Code compilation result', code)
  const runner = new Function(
    ...['prelude', 'self', 'code', 'context'],
    'with (prelude) { with (self) { return [ eval(code) ] } }'
  )
  const self = await getCodeExecutionContext(context)
  const [value] = runner(require('prelude-ls'), self, code, context)
  const returnedValue = await Promise.resolve(value)
  let result = postProcessResult(returnedValue)
  const extraMessages = [...self.extraMessages]
  return { result, extraMessages }
}

function postProcessResult(returnedValue: any) {
  if (typeof returnedValue === 'string') {
    return returnedValue
  }
  return require('util').inspect(returnedValue)
}

export const CodeEvaluationMessageHandler: TextMessageHandler = (
  text,
  context
) => {
  if (text.startsWith(';')) {
    return async () => {
      const input = text.slice(1)
      try {
        var { result, extraMessages } = await evaluateCode(input, context)
        return [{ type: 'text', text: result }, ...extraMessages]
      } catch (error: any) {
        const stack = String(error.stack).replace(
          /\/evalaas\/webpack:\/@dtinth\/automatron-core\//g,
          ''
        )
        return [{ type: 'text', text: `❌ EVALUATION FAILED ❌\n${stack}` }]
      }
    }
  }
}


================================================
FILE: core/src/Cron.ts
================================================
import { ObjectId } from 'mongodb'
import { getDb } from './MongoDatabase'
import { AutomatronContext } from './types'

interface CronEntry {
  name: string
  scheduledTime: string
  completed: boolean
  notes?: string
}

export async function addCronEntry(
  context: AutomatronContext,
  time: number,
  text: string
) {
  const targetTime = new Date(time)
  targetTime.setUTCSeconds(0)
  targetTime.setUTCMilliseconds(0)

  const collection = await getCronCollection(context)
  await collection.insertOne({
    name: text,
    scheduledTime: targetTime.toISOString(),
    completed: false,
  })

  return {
    localTime: new Date(targetTime.getTime() + 7 * 3600e3)
      .toJSON()
      .replace(/\.000Z/, ''),
  }
}

export async function getCronCollection(context: AutomatronContext) {
  const db = await getDb(context)
  return db.collection<CronEntry>('cronJobs')
}

export async function getPendingCronJobs(context: AutomatronContext) {
  const collection = await getCronCollection(context)
  return collection.find({ completed: false }).toArray()
}

export async function updateCronJob(
  context: AutomatronContext,
  jobId: string,
  update: Partial<CronEntry>
) {
  const collection = await getCronCollection(context)
  await collection.updateOne({ _id: new ObjectId(jobId) }, { $set: update })
}


================================================
FILE: core/src/DataEncryption.ts
================================================
import Encrypted from '@dtinth/encrypted'
import { AutomatronContext } from './types'

export function decrypt(
  context: AutomatronContext,
  encryptedPayload: string
): any {
  const encrypted = Encrypted(context.secrets.ENCRYPTION_SECRET)
  return encrypted(encryptedPayload)
}

export function encrypt(context: AutomatronContext, payload: any): string {
  const encrypted = Encrypted(context.secrets.ENCRYPTION_SECRET)
  return encrypted.encrypt(payload)
}


================================================
FILE: core/src/DeviceTracking.ts
================================================
import { getDb } from './MongoDatabase'
import { ref } from './PersistentState'
import { AutomatronContext } from './types'
import { logger } from './logger'

async function getCollection(context: AutomatronContext) {
  const db = await getDb(context)
  return db.collection<DeviceLogEntry>('deviceLog')
}

interface DeviceLogEntry {
  time: string
  deviceId: string
  key: string
  value: any
}

async function getDeviceStateUpdater(
  context: AutomatronContext,
  deviceId: string
) {
  const collection = await getCollection(context)
  const device = getDeviceRef(context, deviceId)
  const state = (await device.get()) || {}
  const time = new Date().toISOString()
  let changed = false
  return {
    time,
    state,
    update: async (key: string, value: any) => {
      if (JSON.stringify(state[key]) !== JSON.stringify(value)) {
        state[key] = value
        await collection.insertOne({
          time,
          deviceId,
          key,
          value,
        })
        logger.info(
          { deviceId, key, value },
          `Device "${deviceId}" property "${key}" changed to "${value}"`
        )
        changed = true
      }
    },
    updateSilently: async (key: string, value: any) => {
      if (JSON.stringify(state[key]) !== JSON.stringify(value)) {
        state[key] = value
        changed = true
      }
    },
    save: async () => {
      if (changed) {
        await device.set(state)
      }
    },
  }
}

export async function trackDevice(
  context: AutomatronContext,
  deviceId: string,
  properties: Record<string, any>
) {
  const updater = await getDeviceStateUpdater(context, deviceId)
  if (!('ip' in properties)) {
    properties.ip = context.requestInfo.ip
  }
  for (const [key, value] of Object.entries(properties)) {
    await updater.update(key, value)
  }
  await updater.updateSilently('lastSeen', updater.time)
  await updater.save()
  return updater.state
}

function getDeviceRef(context: AutomatronContext, deviceId: string) {
  return ref(context, 'devices.' + deviceId)
}

export async function checkDeviceOnlineStatus(context: AutomatronContext) {
  const deviceIds = await getDeviceIds(context)
  for (const deviceId of deviceIds) {
    const updater = await getDeviceStateUpdater(context, deviceId)
    if (
      updater.state.online &&
      Date.now() - new Date(updater.state.lastSeen).getTime() > 1000 * 60 * 3
    ) {
      await updater.update('online', false)
    }
    await updater.save()
  }
}

async function getDeviceIds(context: AutomatronContext) {
  const value = await ref(context, 'deviceIds').get()
  return (value || '').split(',').filter(Boolean)
}


================================================
FILE: core/src/ExpenseTracking.ts
================================================
import { messagingApi } from '@line/bot-sdk'
import { GristDocAPI } from 'grist-api'
import { ExpenseTrackingGristTables } from './ExpenseTrackingGrist'
import { createBubble } from './LINEMessageUtilities'
import { TypedGristDocAPI } from './TypedGristDocAPI'
import { AutomatronContext } from './types'

type ExpenseRecord = ExpenseTrackingGristTables['Daily_Expenses']

function getGristDoc(context: AutomatronContext) {
  const grist = new GristDocAPI(context.secrets.GRIST_EXPENSE_DOC_ID, {
    server: context.secrets.GRIST_BASE_URL,
    apiKey: context.secrets.GRIST_API_KEY,
  })
  return grist as TypedGristDocAPI<ExpenseTrackingGristTables>
}

export async function recordExpense(
  context: AutomatronContext,
  amount: string,
  category: string,
  remarks = ''
): Promise<messagingApi.FlexMessage> {
  const date = new Date().toJSON().split('T')[0]
  // Airtable
  const doc = getGristDoc(context)
  const [id] = await doc.addRecords('Daily_Expenses', [
    {
      Date: new Date(date + 'T00:00:00Z').getTime() / 1000,
      Category: category,
      Amount: parseInt(amount),
      Note: remarks,
    },
  ])
  const gristUri =
    context.secrets.GRIST_EXPENSE_URI +
    `?openExternalBrowser=1#a1.s12.r${id}.c28`
  const body: messagingApi.FlexBox = {
    type: 'box',
    layout: 'vertical',
    contents: [
      {
        type: 'text',
        text: '฿' + amount,
        size: 'xxl',
        weight: 'bold',
      },
      {
        type: 'text',
        text: `${category}\nrecorded`,
        wrap: true,
      },
    ],
    action: {
      type: 'uri',
      label: 'Open Grist',
      uri: gristUri,
    },
  }
  const footer = await getExpensesSummaryData(context)
  const bubble = createBubble('expense tracking', body, {
    headerColor: '#ffffbb',
    footer: {
      type: 'box',
      layout: 'horizontal',
      spacing: 'sm',
      contents: footer.map(([label, text]) => ({
        type: 'box',
        layout: 'vertical',
        contents: [
          {
            type: 'text',
            text: label,
            color: '#8b8685',
            size: 'xs',
            align: 'end',
          },
          {
            type: 'text',
            text: text,
            color: '#8b8685',
            size: 'sm',
            align: 'end',
          },
        ],
      })),
      action: {
        type: 'uri',
        label: 'Open Grist',
        uri: gristUri,
      },
    },
  })
  return bubble
}
async function getExpensesSummaryData(context: AutomatronContext) {
  const date = new Date().toJSON().split('T')[0]
  const gristToDate = (t: number) =>
    new Date(t * 1000).toISOString().split('T')[0]
  const grist = getGristDoc(context)
  const tableData = await grist.fetchTable('Daily_Expenses')
  const normalRecords = tableData.filter((r) => !r['Occasion'])
  const total = (records: ExpenseRecord[]) =>
    records.map((r) => +r['Amount'] || 0).reduce((a, b) => a + b, 0)
  const firstDate = normalRecords
    .map((r) => gristToDate(r['Date']))
    .reduce((a, b) => (a < b ? a : b), date)
  const todayUsage = total(
    normalRecords.filter((r) => gristToDate(r['Date']) === date)
  )
  const totalUsage = total(normalRecords)
  const dayNumber =
    Math.round((Date.parse(date) - Date.parse(firstDate)) / 86400e3) + 1
  const [pacemakerPerDay, pacemakerBase] =
    context.secrets.EXPENSE_PACEMAKER.split('/')
  const pacemaker = +pacemakerBase + +pacemakerPerDay * dayNumber - totalUsage
  const $ = (v: number) => `฿${v.toFixed(0)}`
  return [
    ['today', $(todayUsage)],
    ['pace', $(pacemaker)],
    ['day', `${dayNumber}`],
  ]
}


================================================
FILE: core/src/ExpenseTrackingGrist.ts
================================================
export namespace grist {
  export type Text = string
  export type Numeric = number
  export type Int = number
  export type Bool = boolean
  export type Date = number
  export type DateTime = string
  export type Choice = string
  export type Reference = number
  export type ReferenceList = ['L', ...number[]] | null
  export type ChoiceList = ['L', ...string[]] | null
  export type Attachments = ['L', ...number[]] | null
  export type Any = any
}

export type ExpenseTrackingGristTables = {
  Daily_Expenses: {
    id: number
    Date: grist.Date
    Amount: grist.Numeric
    Category: grist.Choice
    Beneficiary: grist.Choice
    Note: grist.Text
    Occasion: grist.Bool
    Month: grist.Any // calculated/formula
    Daily_Amount: grist.Numeric // calculated/formula
  }
}


================================================
FILE: core/src/HomeAutomation.ts
================================================
import Encrypted from '@dtinth/encrypted'
import { AutomatronContext } from './types'
import axios from 'axios'

export async function sendHomeCommand(
  context: AutomatronContext,
  cmd: string | string[]
): Promise<void> {
  const cmds = Array.isArray(cmd) ? cmd : [cmd]
  const encrypted = Encrypted(context.secrets.ENCRYPTION_SECRET)
  const { url, key } = encrypted`mz8Dc0LiBPPI63I5lMJHdC3RC9VF//S2.QYg30oYhuEUS8b1u80vM7sO0cv4OLYNtxy52fTMnf2Vcg8QZHlLeXUV1qPnEjR/5jYTid5hG6of8mHDHjTE1A+luDzplgM4WJQBgDNM2pkRnKnbcmAUw8MXxBb4ZMqrrAyFELigKoELWwbDg51ErhFXrm+n3hzfpbRIcge1BdH6aEPtNipsUcMj7q7BfBqFLZA==`
  await Promise.all(
    cmds.map(async (command) => {
      const id =
        new Date().toJSON() +
        Math.floor(Math.random() * 10000)
          .toString()
          .padStart(2, '0')
      await axios.post(url, { id, topic: 'home', data: command }, {
        headers: {
          'X-Api-Key': key
        }
      })
    })
  )
}


================================================
FILE: core/src/ImageMessageHandler.ts
================================================
import vision from '@google-cloud/vision'
import { ref } from './PersistentState'
import { getBlob, getBlobUrl } from './TemporaryBlobStorage'
import { TextMessage, TextMessageHandler } from './types'

export const ImageMessageHandler: TextMessageHandler = (text, context) => {
  if (text.startsWith('image:')) {
    return async () => {
      const blobName = text.slice(6)
      await ref(context, 'latestImage').set(blobName)
      return [{ type: 'text', text: blobName }]
    }
  }
  if (text === 'annotate') {
    return async () => {
      const blobName = await ref(context, 'latestImage').get()
      return await annotateImage(blobName)
    }
  }
  if (text === 'image url') {
    return async () => {
      const blobName = await ref(context, 'latestImage').get()
      return await getBlobUrl(blobName)
    }
  }
}

async function annotateImage(blobName: string) {
  const buffer = await getBlob(blobName)
  const imageAnnotator = new vision.ImageAnnotatorClient()
  const results = await imageAnnotator.documentTextDetection(buffer)
  const fullTextAnnotation = results[0].fullTextAnnotation
  const blocks: string[] = []
  for (const page of fullTextAnnotation.pages) {
    blocks.push(
      ...page.blocks.map((block) => {
        return block.paragraphs
          .map((p) =>
            p.words.map((w) => w.symbols.map((s) => s.text).join('')).join(' ')
          )
          .join('\n\n')
      })
    )
  }
  const blocksToResponses = (blocks: string[]) => {
    if (blocks.length <= 4) return blocks
    let processedIndex = 0
    const outBlocks = []
    for (let i = 0; i < 4; i++) {
      const targetIndex = Math.ceil(((i + 1) * blocks.length) / 4)
      outBlocks.push(
        blocks
          .slice(processedIndex, targetIndex)
          .map((x) => `・ ${x}`)
          .join('\n')
      )
      processedIndex = targetIndex
    }
    return outBlocks
  }
  const responses = blocksToResponses(blocks)
  return [...responses.map((r): TextMessage => ({ type: 'text', text: r }))]
}


================================================
FILE: core/src/LINEClient.ts
================================================
import { messagingApi } from '@line/bot-sdk'

export class LINEClient {
  private readonly api: messagingApi.MessagingApiClient
  private readonly blobApi: messagingApi.MessagingApiBlobClient
  constructor(config: { channelAccessToken: string }) {
    this.api = new messagingApi.MessagingApiClient({
      channelAccessToken: config.channelAccessToken,
    })
    this.blobApi = new messagingApi.MessagingApiBlobClient({
      channelAccessToken: config.channelAccessToken,
    })
  }
  replyMessage(replyToken: string, messages: messagingApi.Message[]) {
    return this.api.replyMessage({ replyToken, messages })
  }
  pushMessage(to: string, messages: messagingApi.Message[]) {
    return this.api.pushMessage({ to, messages })
  }
  getMessageContent(messageId: string) {
    return this.blobApi.getMessageContent(messageId)
  }
  showLoadingAnimation(chatId: string) {
    return this.api.showLoadingAnimation({ chatId })
  }
}


================================================
FILE: core/src/LINEMessageUtilities.ts
================================================
import { messagingApi } from '@line/bot-sdk'

export function toMessages(data: any): messagingApi.Message[] {
  if (!data) data = '...'
  if (typeof data === 'string') data = [{ type: 'text', text: data }]
  if (!Array.isArray(data)) data = [data]
  return data
}

export function createBubble(
  title: string,
  text: string | messagingApi.FlexBox,
  {
    headerBackground = '#353433',
    headerColor = '#d7fc70',
    textSize = 'xl',
    altText = String(text),
    footer,
  }: {
    headerBackground?: string
    headerColor?: string
    textSize?: messagingApi.FlexText['size']
    altText?: string
    footer?: string | messagingApi.FlexBox
  } = {}
): messagingApi.FlexMessage {
  const data: messagingApi.FlexContainer = {
    type: 'bubble',
    styles: {
      header: { backgroundColor: headerBackground },
    },
    header: {
      type: 'box',
      layout: 'vertical',
      contents: [
        { type: 'text', text: title, color: headerColor, weight: 'bold' },
      ],
    },
    body:
      typeof text === 'string'
        ? {
            type: 'box',
            layout: 'vertical',
            contents: [
              { type: 'text', text: text, wrap: true, size: textSize },
            ],
          }
        : text,
  }
  if (footer) {
    data.styles!.footer = { backgroundColor: '#e9e8e7' }
    data.footer =
      typeof footer === 'string'
        ? {
            type: 'box',
            layout: 'vertical',
            contents: [
              {
                type: 'text',
                text: footer,
                wrap: true,
                size: 'sm',
                color: '#8b8685',
              },
            ],
          }
        : footer
  }
  return {
    type: 'flex',
    altText: truncate(`[${title}] ${altText}`, 400),
    contents: data,
  }
}

function truncate(text: string, maxLength: number) {
  return text.length + 5 > maxLength
    ? text.substr(0, maxLength - 5) + '…'
    : text
}


================================================
FILE: core/src/LanguageModelAssistant.ts
================================================
import axios from 'axios'
import { decrypt } from './DataEncryption'
import { logger } from './logger'
import { getDb } from './MongoDatabase'
import { ref } from './PersistentState'
import {
  AutomatronContext,
  AutomatronResponse,
  TextMessageHandler,
} from './types'

type Role = 'system' | 'user' | 'assistant'
interface ChatMessage {
  role: Role
  content: string
}
interface LlmHistoryEntry {
  time: string
  contextMessages?: ChatMessage[]
  inText: string
  outText: string
}

async function getCollection(context: AutomatronContext) {
  const db = await getDb(context)
  return db.collection<LlmHistoryEntry>('llmHistory')
}

export const LanguageModelAssistantMessageHandler: TextMessageHandler = (
  text,
  context
) => {
  const runLlm = async (
    inText: string,
    continueFrom?: LlmHistoryEntry
  ): Promise<AutomatronResponse> => {
    const key = decrypt(
      context,
      'c0wgjM3RJp/V40lLhcHbtjUDBvlT/NlI.yF9iWwImsHrUOiZkD6UMZdGyjLd3yCJm7WMkN6dxerzXlxCK4U6bSzNFHwyvUjPDop4+gLCs5Qa9Pcxwii3DvSVjjE+7'
    )
    const nowIct = new Date(Date.now() + 7 * 3600e3)
    const day = [
      'Sunday',
      'Monday',
      'Tuesday',
      'Wednesday',
      'Thursday',
      'Friday',
      'Saturday',
    ][nowIct.getDay()]

    const prompt: string[] = [
      // https://beta.openai.com/examples/default-chat
      'Current date and time: ' +
        nowIct.toISOString().replace('Z', '+07:00') +
        ' (Asia/Bangkok).',
      'Today is ' + day + '.',
      'The following is a conversation with an AI assistant, automatron. ' +
        'The assistant is helpful, creative, clever, funny, and very friendly. ' +
        (await ref(context, 'llmPrompt').get()),
    ]
    const newContext: ChatMessage[] = [
      ...(continueFrom
        ? [
            ...(continueFrom.contextMessages ?? []),
            { role: 'user' as Role, content: continueFrom.inText },
            { role: 'assistant' as Role, content: continueFrom.outText },
          ]
        : []),
    ]
    const messages: ChatMessage[] = [
      { role: 'system', content: prompt.join('\n') },
      ...newContext,
      { role: 'user', content: inText },
    ]
    const payload = {
      model: 'chatgpt-4o-latest',
      messages,
      temperature: +(await ref(context, 'llmTemperature').get()) || 0.5,
    }
    const response = await axios.post(
      'https://api.openai.com/v1/chat/completions',
      payload,
      { headers: { Authorization: `Bearer ${key}` } }
    )
    const responseText = response.data.choices[0].message.content.trim()
    logger.info(
      { assistant: { prompt, response: response.data } },
      'Ran OpenAI assistant'
    )
    const collection = await getCollection(context)
    await collection.insertOne({
      time: new Date().toISOString(),
      contextMessages: newContext,
      inText,
      outText: responseText,
    })
    return [{ type: 'text', text: responseText }]
  }

  if (text.match(/^hey\b/i)) {
    return async () => {
      return runLlm(text)
    }
  }
  if (text.match(/^hmm?\b/i)) {
    return async () => {
      const collection = await getCollection(context)
      const [lastEntry] = await collection
        .find({})
        .sort({ _id: -1 })
        .limit(1)
        .toArray()
      return runLlm(text, lastEntry)
    }
  }
}


================================================
FILE: core/src/MessageHandler.ts
================================================
import { CodeEvaluationMessageHandler } from './CodeEvaluation'
import { addCronEntry } from './Cron'
import { recordExpense } from './ExpenseTracking'
import { sendHomeCommand } from './HomeAutomation'
import { ImageMessageHandler } from './ImageMessageHandler'
import { LanguageModelAssistantMessageHandler } from './LanguageModelAssistant'
import {
  MessageHistoryMessageHandler,
  saveMessageHistory,
} from './MessageHistory'
import { getDb } from './MongoDatabase'
import { ref } from './PersistentState'
import { PhoneFinderMessageHandler } from './PhoneFinder'
import { getCodeExecutionContext } from './PreludeCode'
import { decodeRomanNumerals } from './RomanNumerals'
import { SpendingTrackingMessageHandler } from './SpendingTracking'
import { putBlob } from './TemporaryBlobStorage'
import { trace } from './Tracing'
import { AutomatronContext, AutomatronResponse } from './types'

const messageHandlers = [
  CodeEvaluationMessageHandler,
  ImageMessageHandler,
  MessageHistoryMessageHandler,
  SpendingTrackingMessageHandler,
  PhoneFinderMessageHandler,
  LanguageModelAssistantMessageHandler,
]

export async function handleTextMessage(
  context: AutomatronContext,
  message: string,
  options: { source: string }
): Promise<AutomatronResponse> {
  message = message.trim()
  let match: RegExpMatchArray | null

  context.addPromise(
    'Save text history',
    getDb(context).then((db) =>
      trace(context, 'Save history', () =>
        saveMessageHistory(context, message, options.source)
      )
    )
  )

  if (message === 'ac on' || message === 'sticker:2:27') {
    await sendHomeCommand(context, 'ac on')
    return 'ok, turning air-con on'
  } else if (message === 'ac off' || message === 'sticker:2:29') {
    await sendHomeCommand(context, 'ac off')
    return 'ok, turning air-con off'
  } else if (message === 'power on' || message === 'plugs on') {
    await sendHomeCommand(context, 'plugs on')
    return 'ok, turning smart plugs on'
  } else if (message === 'power off' || message === 'plugs off') {
    await sendHomeCommand(context, 'plugs off')
    return 'ok, turning smart plugs off'
  } else if (
    message === 'home' ||
    message === 'arriving' ||
    message === 'sticker:2:503'
  ) {
    await sendHomeCommand(context, ['plugs on', 'lights normal', 'ac on'])
    return 'preparing home'
  } else if (message === 'leaving' || message === 'sticker:2:502') {
    await sendHomeCommand(context, ['plugs off', 'lights off', 'ac off'])
    return 'bye'
  } else if (message === 'lights' || message === 'sticker:4:275') {
    await sendHomeCommand(context, 'lights normal')
    return 'ok, lights normal'
  } else if (
    message === 'bedtime' ||
    message === 'gn' ||
    message === 'gngn' ||
    message === 'sticker:11539:52114128'
  ) {
    await sendHomeCommand(context, 'lights dimmed')
    await addCronEntry(context, Date.now() + 300e3, 'lights off')
    const prelude = await getCodeExecutionContext(context)
    await prelude.executeHandlers('bedtime')
    return 'ok, good night'
  } else if (message === 'ooo') {
    const prelude = await getCodeExecutionContext(context)
    await prelude.executeHandlers('ooo')
    return 'ok, out of office'
  } else if (message === 'work') {
    const prelude = await getCodeExecutionContext(context)
    await prelude.executeHandlers('work')
    return 'ok, set working status'
  } else if ((match = message.match(/^lights (\w+)$/))) {
    const cmd = match[1]
    await sendHomeCommand(context, 'lights ' + cmd)
    return 'ok, lights ' + cmd
  } else if ((match = message.match(/^in ([\d\.]+)([mh]),?\s+([^]+)$/))) {
    const targetTime =
      Date.now() + +match[1] * (match[2] === 'm' ? 60 : 3600) * 1e3
    const result = await addCronEntry(context, targetTime, match[3])
    return `will run "${match[3]}" at ${result.localTime}`
  } else if ((match = message.match(/^([\d.]+|[ivxlcdm]+)(j?)([tfghmol])$/i))) {
    const m = match
    const enteredAmount = m[1].match(/[ivxlcdm]/)
      ? decodeRomanNumerals(m[1])
      : +m[1]
    const conversionRate = m[2] ? 0.302909 : 1
    const amount = (enteredAmount * conversionRate).toFixed(2)
    const category = (
      {
        f: 'Food',
        t: 'Transportation',
        s: 'Stationery',
        g: 'Grocery',
        h: 'Hygiene',
        m: 'Medicine',
        w: 'Wellness',
        // o: 'occasion',
        // l: 'lodging',
      } as {
        [k: string]: string
      }
    )[m[3].toLowerCase()]
    const remarks = m[2] ? `${m[1]} JPY` : ''
    return await recordExpense(context, amount, category, remarks)
  } else if ((match = message.match(/^([ivxlcdm]+)$/i))) {
    return `${match[1]} = ${decodeRomanNumerals(match[1])}`
  }

  // Go through message handlers and see if any of them can handle the message
  for (const handler of messageHandlers) {
    const action = handler(message, context)
    if (action) {
      return action()
    }
  }

  // At this point, the message is not recognized.
  // Just save it to the stack.
  const size = await ref(context, 'stack').push(message)
  return '(unrecognized message, saved to stack (size=' + size + '))'
}

export async function handleImage(
  context: AutomatronContext,
  imageBuffer: Buffer,
  options: { source: string }
) {
  const blobName = await putBlob(imageBuffer, '.jpg')
  return await handleTextMessage(context, 'image:' + blobName, options)
}


================================================
FILE: core/src/MessageHistory.ts
================================================
import { getDb } from './MongoDatabase'
import { AutomatronContext, TextMessage, TextMessageHandler } from './types'

export interface MessageHistoryEntry {
  time: string
  text: string
  source: string
}

export async function saveMessageHistory(
  context: AutomatronContext,
  text: string,
  source: string
) {
  const db = await getDb(context)
  await db.collection<MessageHistoryEntry>('history').insertOne({
    time: new Date().toISOString(),
    text: text,
    source: source,
  })
}

export async function getMessageHistory(
  context: AutomatronContext,
  options: { limit: number }
) {
  const db = await getDb(context)
  return await db
    .collection<MessageHistoryEntry>('history')
    .find({})
    .sort({ _id: -1 })
    .limit(options.limit)
    .toArray()
}

export const MessageHistoryMessageHandler: TextMessageHandler = (
  text,
  context
) => {
  if (text === 'history') {
    return async () => {
      const history = await getMessageHistory(context, { limit: 5 })
      return [...history]
        .reverse()
        .map((entry): TextMessage => ({ type: 'text', text: entry.text }))
    }
  }
}


================================================
FILE: core/src/MongoDatabase.ts
================================================
import { Db, MongoClient } from 'mongodb'
import { trace } from './Tracing'
import { AutomatronContext } from './types'

export { Db } from 'mongodb'

const globalCacheKey = Symbol.for('automatron/MongoDatabase')
const cache: { dbPromise: Promise<Db> | null } = (() => {
  return ((global as any)[globalCacheKey] = (global as any)[globalCacheKey] || {
    dbPromise: null,
  })
})()

export async function getDb(context: AutomatronContext): Promise<Db> {
  if (cache.dbPromise) {
    return cache.dbPromise
  }
  cache.dbPromise = trace(context, 'getDb', async () => {
    const client = await MongoClient.connect(context.secrets.MONGODB_URL)
    const db = client.db('automatron')
    return db
  })
  cache.dbPromise.catch(() => {
    cache.dbPromise = null
  })
  return cache.dbPromise
}


================================================
FILE: core/src/NotificationProcessor.ts
================================================
import { getDb } from './MongoDatabase'
import { AutomatronContext } from './types'
import { logger } from './logger'

export interface INotification {
  packageName: string
  text: string
  title: string
  time: string
  postTime: string
  when: string
  key: string
}

export async function handleNotification(
  context: AutomatronContext,
  notification: INotification
) {
  const text = notification.title + ' : ' + notification.text
  const time = notification.when || notification.postTime || notification.time
  const key = notification.key
  const promises: Promise<void>[] = []
  const process = (name: string, promise: Promise<void>) => {
    promises.push(
      promise.catch((err) => {
        logger.error({ err, name }, `Unable to run processor "${name}": ${err}`)
      })
    )
  }

  process('save to DB', saveNotificationToDb(context, notification))

  if (notification.packageName === 'com.kasikorn.retail.mbanking.wap') {
    process('process KBank', handleKbankNotification(context, key, text, time))
  }

  return Promise.all(promises)
}

async function saveNotificationToDb(
  context: AutomatronContext,
  notification: INotification
) {
  const db = await getDb(context)
  await db
    .collection('notis')
    .insertOne({ received: new Date().toISOString(), ...notification })
}

export async function handleKbankNotification(
  context: AutomatronContext,
  key: string,
  text: string,
  time: string = new Date().toISOString()
) {
  let m: RegExpMatchArray | null

  m = text.match(
    /^รายการใช้บัตร : หมายเลขบัตร (\S+) จำนวนเงิน (\S+) (\S+) ที่ ([^]*)$/
  )
  if (m) {
    const db = await getDb(context)
    db.collection('txs').insertOne({
      notificationKey: key,
      time,
      type: 'charge',
      card: m[1],
      amount: parseAmount(m[2]),
      currency: m[3],
      merchant: m[4],
    })
    return
  }

  m = text.match(
    /^รายการยกเลิก : หมายเลขบัตร (\S+) จำนวนเงิน (\S+) (\S+) ที่ ([^]*)$/
  )
  if (m) {
    const db = await getDb(context)
    db.collection('txs').insertOne({
      notificationKey: key,
      time,
      type: 'refund',
      card: m[1],
      amount: parseAmount(m[2]),
      currency: m[3],
      merchant: m[4],
    })
    return
  }
}

function parseAmount(text: string) {
  return +text.replace(/,/g, '')
}


================================================
FILE: core/src/PersistentState.ts
================================================
import { getDb } from './MongoDatabase'
import { trace } from './Tracing'
import { AutomatronContext } from './types'

interface StateDoc {
  _id: string
  value: any
}

interface StateDocStack extends StateDoc {
  value: any[]
}

async function push(
  context: AutomatronContext,
  key: string,
  value: any
): Promise<number> {
  const db = await getDb(context)
  const result = await trace(context, `read(${key})`, () =>
    db
      .collection<StateDocStack>('state')
      .findOneAndUpdate(
        { _id: key },
        { $push: { value } },
        { upsert: true, returnDocument: 'after' }
      )
  )
  return result.value!.value.length
}

async function pop(context: AutomatronContext, key: string): Promise<any> {
  const db = await getDb(context)
  const result = await trace(context, `pop(${key})`, () =>
    db
      .collection<StateDocStack>('state')
      .findOneAndUpdate({ _id: key }, { $pop: { value: 1 } })
  )
  return result.value!.value.pop()
}

async function get(context: AutomatronContext, key: string): Promise<any> {
  const db = await getDb(context)
  const result = await trace(context, `get(${key})`, () =>
    db.collection<StateDoc>('state').findOne({ _id: key })
  )
  return result?.value
}

async function set(
  context: AutomatronContext,
  key: string,
  value: any
): Promise<boolean> {
  const db = await getDb(context)
  const result = await trace(context, `set(${key})`, () =>
    db
      .collection<StateDoc>('state')
      .findOneAndUpdate(
        { _id: key },
        { $set: { value } },
        { upsert: true, returnDocument: 'after' }
      )
  )
  return !!result.ok
}

export function ref(context: AutomatronContext, key: string) {
  return {
    push: push.bind(null, context, key),
    pop: pop.bind(null, context, key),
    get: get.bind(null, context, key),
    set: set.bind(null, context, key),
  }
}


================================================
FILE: core/src/PhoneFinder.ts
================================================
import { TextMessageHandler } from './types'
import { GoogleAuth } from 'google-auth-library'
import axios from 'axios'
import { decrypt } from './DataEncryption'

const auth = new GoogleAuth()

export const PhoneFinderMessageHandler: TextMessageHandler = (
  text,
  context
) => {
  if (text === 'where is my phone') {
    return async () => {
      const url = decrypt(
        context,
        '09oTErU3ru/YqzCNmUBIH2ftVYz1jSfG.NODxXCib7BiuF0vAtnxpwbsvVQ5fxNVXoYDYGZHmJNxBRLPfAWj9KmAK89cOjCtFw3uxudSjqCV1F79Icmn3/SWwi5Z313xXkKyJFr9YWT/1dq8+EWcZfbPR'
      )
      const client = await auth.getIdTokenClient(url)
      const jwt = await client.idTokenProvider.fetchIdToken(url)
      await axios.post(url, {}, { headers: { Authorization: `Bearer ${jwt}` } })
      return 'calling you now...'
    }
  }
}


================================================
FILE: core/src/PreludeCode.ts
================================================
import axios from 'axios'
import { Storage } from '@google-cloud/storage'
import { AutomatronContext } from './types'
import tweetnacl from 'tweetnacl'
import jsonwebtoken from 'jsonwebtoken'
import crypto from 'crypto'
import util from 'util'
import Encrypted from '@dtinth/encrypted'
import { logger } from './logger'
import * as mongodb from 'mongodb'
import * as os from 'os'
import { Db, getDb } from './MongoDatabase'
import * as NotificationProcessor from './NotificationProcessor'
import * as PersistentState from './PersistentState'
import { getAllSpeedDials, getSpeedDialCode, saveSpeedDial } from './SpeedDial'

const lib = require('lib')
const storage = new Storage()
const preludeFile = storage.bucket('dtinth-automatron-data').file('prelude.js')

export async function deployPrelude(context: AutomatronContext) {
  const userPreludeResponse = await axios.get(
    'https://api.github.com/repos/dtinth/automatron-prelude/contents/prelude.js',
    {
      auth: {
        username: context.secrets.GITHUB_OAUTH_APP_CREDENTIALS.split(':')[0],
        password: context.secrets.GITHUB_OAUTH_APP_CREDENTIALS.split(':')[1],
      },
    }
  )
  const buffer = Buffer.from(userPreludeResponse.data.content, 'base64')
  await preludeFile.save(buffer)
  await PersistentState.ref(context, 'preludeDeployedAt').set(
    new Date().toISOString()
  )
}

let cache: { deployedAt: string; code: string } | undefined

export async function getPreludeCode(context: AutomatronContext) {
  const latestDeployedAt = await PersistentState.ref(
    context,
    'preludeDeployedAt'
  ).get()
  if (cache && cache.deployedAt === latestDeployedAt) {
    return cache.code
  }
  const [buffer] = await preludeFile.download()
  const code = buffer.toString('utf8')
  cache = {
    deployedAt: latestDeployedAt,
    code,
  }
  return code
}

export async function getCodeExecutionContext(
  context: AutomatronContext
): Promise<any> {
  // Prepare "self" context
  const self: any = {}
  self.exec = (code: string) => {
    return new Function(
      ...['self', 'code'],
      'with (self) { return eval(code) }'
    )(self, code)
  }
  self.encrypted = Encrypted(context.secrets.ENCRYPTION_SECRET)
  self.extraMessages = []
  self.withDb = (f: (db: Db) => any) => getDb(context).then(f)
  self.ref = PersistentState.ref.bind(null, context)
  self.stack = self.ref('stack')
  self.require = (id: string) => {
    const availableModules: { [id: string]: any } = {
      axios,
      tweetnacl,
      jsonwebtoken,
      crypto,
      util,
      mongodb,
      os,
      lib,
      '@/NotificationProcessor': NotificationProcessor,
    }
    const available = {}.hasOwnProperty.call(availableModules, id)
    if (!available) {
      throw new Error(
        `Module ${id} not available; available modules: ${Object.keys(
          availableModules
        )}`
      )
    }
    return availableModules[id]
  }
  self.json = (x: any) => JSON.stringify(x, null, 2)
  self.SD = async (name?: string, f?: () => any) => {
    if (name && f) {
      if (typeof f !== 'function') {
        throw new Error('f must be a function')
      }
      await saveSpeedDial(context, name, f.toString())
      return 'Saved speed dial'
    } else if (name) {
      const code = await getSpeedDialCode(context, name)
      self.extraMessages.push({
        type: 'text',
        text: `;;SD('${name}', ${code})`,
      })
      return self.exec(code)()
    } else {
      const all = await getAllSpeedDials(context)
      return all.map((s) => s._id)
    }
  }

  // Plugin system
  type Handler = (...args: any[]) => Promise<void>
  const registeredHandlers: Record<string, Set<Handler>> = {}
  self.registerHandler = (event: string, handler: Handler) => {
    if (!registeredHandlers[event]) {
      registeredHandlers[event] = new Set()
    }
    registeredHandlers[event].add(handler)
  }
  self.executeHandlers = async (event: string, ...args: any[]) => {
    await Promise.all(
      [...(registeredHandlers[event] || [])].map(async (handler, i) => {
        try {
          const start = Date.now()
          await handler(...args)
          const elapsed = Date.now() - start
          logger.info(
            `Done executing handler index ${i} for event ${event} in ${elapsed} ms`
          )
        } catch (error) {
          logger.error(
            { err: error },
            `Unable to execute handler index ${i} for event ${event}: ${error}`
          )
        }
      })
    )
  }

  // Execute user prelude
  const userPrelude = await getPreludeCode(context)
  self.exec(userPrelude)

  return self
}


================================================
FILE: core/src/RomanNumerals.ts
================================================
export function decodeRomanNumerals(romanNumerals: string) {
  const decode = (s: string): number => {
    if (s.startsWith('M')) return 1000 + decode(s.substr(1))
    if (s.startsWith('CM')) return 900 + decode(s.substr(2))
    if (s.startsWith('D')) return 500 + decode(s.substr(1))
    if (s.startsWith('CD')) return 400 + decode(s.substr(2))
    if (s.startsWith('C')) return 100 + decode(s.substr(1))
    if (s.startsWith('XC')) return 90 + decode(s.substr(2))
    if (s.startsWith('L')) return 50 + decode(s.substr(1))
    if (s.startsWith('XL')) return 40 + decode(s.substr(2))
    if (s.startsWith('X')) return 10 + decode(s.substr(1))
    if (s.startsWith('IX')) return 9 + decode(s.substr(2))
    if (s.startsWith('V')) return 5 + decode(s.substr(1))
    if (s.startsWith('IV')) return 4 + decode(s.substr(2))
    if (s.startsWith('I')) return 1 + decode(s.substr(1))
    if (s === '') return 0
    throw new InvalidRomanNumeralError(s)
  }
  try {
    return decode(romanNumerals.toUpperCase())
  } catch (e) {
    if (e instanceof InvalidRomanNumeralError) {
      throw new InvalidRomanNumeralError(romanNumerals)
    }
    throw e
  }
}

class InvalidRomanNumeralError extends Error {
  constructor(s: string) {
    super('Invalid roman numeral in input ' + s)
  }
}


================================================
FILE: core/src/SMSHandler.ts
================================================
import { messagingApi, QuickReplyItem } from '@line/bot-sdk'
import { AutomatronContext } from './types'
import { recordExpense } from './ExpenseTracking'
import { createBubble } from './LINEMessageUtilities'
import { LINEClient } from './LINEClient'

export async function handleSMS(
  context: AutomatronContext,
  client: LINEClient,
  text: string
) {
  const { parseSMS } = require('transaction-parser-th')
  const result = parseSMS(text)
  if (!result || !result.amount) return { match: false }
  console.log('SMS parsing result', result)
  const title = result.type
  const pay = result.type === 'pay'
  const moneyOut = ['pay', 'transfer', 'withdraw'].includes(result.type)
  const body: messagingApi.FlexBox = {
    type: 'box',
    layout: 'vertical',
    contents: [
      {
        type: 'text',
        text: '฿' + result.amount,
        size: 'xxl',
        weight: 'bold',
      },
    ],
  }
  const ordering = ['provider', 'from', 'to', 'via', 'date', 'time', 'balance']
  const skip = ['type', 'amount']
  const getOrder = (key: string) => ordering.indexOf(key) + 1 || 999
  for (const key of Object.keys(result)
    .filter((key) => !skip.includes(key))
    .sort((a, b) => getOrder(a) - getOrder(b))) {
    body.contents.push({
      type: 'box',
      layout: 'horizontal',
      spacing: 'md',
      contents: [
        {
          type: 'text',
          text: key,
          align: 'end',
          color: '#888888',
          flex: 2,
        },
        {
          type: 'text',
          text: String(result[key]),
          flex: 5,
        },
      ],
    })
  }
  const quickReply = (suffix: string, label: string): QuickReplyItem => ({
    type: 'action',
    action: {
      type: 'message',
      label: label,
      text: result.amount + suffix,
    },
  })
  const messages: messagingApi.Message[] = [
    {
      ...createBubble(title, body, {
        headerBackground: pay ? '#91918F' : moneyOut ? '#DA9E00' : '#9471FF',
        headerColor: '#FFFFFF',
        altText: require('util').inspect(result),
      }),
      quickReply: {
        items: [
          quickReply('f', 'food'),
          quickReply('h', 'health'),
          quickReply('t', 'transport'),
          quickReply('m', 'misc'),
          quickReply('o', 'occasion'),
        ],
      },
    },
  ]
  if (result.type === 'pay' && result.to === 'LINEPAY*BTS01') {
    messages.push(
      await recordExpense(context, result.amount, 'transportation', 'BTS')
    )
  }
  await client.pushMessage(context.secrets.LINE_USER_ID, messages)
  return { match: true }
}


================================================
FILE: core/src/SlackMessageUtilities.ts
================================================
import { KnownBlock, MessageAttachment } from '@slack/types'

export type SlackMessage = SlackMessageWithoutBlocks | SlackMessageWithBlocks
export type SlackMessageWithoutBlocks = {
  text: string
  attachments?: MessageAttachment[]
}
export type SlackMessageWithBlocks = { text?: string; blocks: KnownBlock[] }

export function createErrorMessage(error: Error): SlackMessage {
  const title =
    (error.name || 'Error') + (error.message ? `: ${error.message}` : '')
  return {
    text: title,
    attachments: [
      {
        color: 'danger',
        blocks: [
          {
            type: 'section',
            text: {
              type: 'mrkdwn',
              text: ['```', String(error.stack || error), '```'].join('')
            }
          }
        ]
      }
    ]
  }
}


================================================
FILE: core/src/SpeedDial.ts
================================================
import { getDb } from './MongoDatabase'
import { AutomatronContext } from './types'

async function getCollection(context: AutomatronContext) {
  const db = await getDb(context)
  return db.collection<{ _id: string; code: string }>('speedDial')
}

export async function saveSpeedDial(
  context: AutomatronContext,
  name: string,
  code: string
) {
  const collection = await getCollection(context)
  await collection.updateOne(
    { _id: name },
    { $set: { code } },
    { upsert: true }
  )
}

export async function getSpeedDialCode(
  context: AutomatronContext,
  name: string
) {
  const collection = await getCollection(context)
  const result = await collection.findOne({ _id: name })
  if (!result) {
    throw new Error(`Speed dial ${name} not found`)
  }
  return result.code
}

export async function getAllSpeedDials(context: AutomatronContext) {
  const collection = await getCollection(context)
  return collection.find({}).sort({ _id: 1 }).toArray()
}


================================================
FILE: core/src/SpendingTracking.ts
================================================
import { getDb } from './MongoDatabase'
import { ref } from './PersistentState'
import { TextMessageHandler } from './types'

export const SpendingTrackingMessageHandler: TextMessageHandler = (
  text,
  context
) => {
  if (text === 'pace') {
    return async () => {
      const db = await getDb(context)
      const paceResetTimeRef = ref(context, 'SpendingTracking.paceResetTime')
      const paceResetTime = await paceResetTimeRef.get()
      const allowancePerMillis = 24000 / (32 * 86400e3)
      const elapsed = Date.now() - Date.parse(paceResetTime)
      const txs = await db
        .collection('txs')
        .find({ time: { $gt: paceResetTime } })
        .sort({ _id: 1 })
        .toArray()
      const warnings = []
      let sum = 0
      for (const tx of txs) {
        let currencyMul
        if (tx.currency === 'บาท') {
          currencyMul = 1
        } else {
          warnings.push(`Unknown currency ${tx.currency}`)
          continue
        }
        const typeMul = tx.type === 'refund' ? -1 : 1
        sum += tx.amount * typeMul * currencyMul
      }
      const remaining = Math.round(allowancePerMillis * elapsed - sum)
      // return { sum, warnings }
      return `used ${sum} บาท\nremaining ${remaining} บาท`
    }
  }
  if (text === 'reset pace') {
    return async () => {
      const now = new Date().toISOString()
      await ref(context, 'SpendingTracking.paceResetTime').set(now)
      return `Pace reset to ${now}`
    }
  }
}


================================================
FILE: core/src/TemporaryBlobStorage.ts
================================================
import { Storage } from '@google-cloud/storage'
import { nanoid } from 'nanoid'

const storage = new Storage()
let latest: { blobName: string; buffer: Buffer } | undefined

export async function putBlob(buffer: Buffer, extension: string) {
  const blobName = nanoid() + extension
  await storage.bucket('tmpblob').file(blobName).save(buffer)
  latest = { blobName, buffer }
  return blobName
}

export async function getBlob(blobName: string) {
  if (latest && latest.blobName === blobName) {
    return latest.buffer
  }
  const response = await storage.bucket('tmpblob').file(blobName).download()
  return response[0]
}

export async function getBlobUrl(blobName: string) {
  const result = await storage
    .bucket('tmpblob')
    .file(blobName)
    .getSignedUrl({
      action: 'read',
      expires: new Date(Date.now() + 86400e3),
      version: 'v4',
      virtualHostedStyle: true,
    })
  return result[0]
}


================================================
FILE: core/src/Tracing.ts
================================================
import { AutomatronContext } from './types'

export async function trace<T>(
  context: AutomatronContext,
  name: string,
  f: () => Promise<T>
) {
  const tracer = context.tracer
  if (!tracer) {
    return f()
  }
  const span = tracer.createChildSpan({ name })
  try {
    return await f()
  } finally {
    span.endSpan()
  }
}


================================================
FILE: core/src/TypedGristDocAPI.ts
================================================
import type { GristDocAPI } from 'grist-api'

// From: https://dt.in.th/GristTypeGenerator
export interface TypedGristDocAPI<Tables extends AnyTables>
  extends Omit<
    GristDocAPI,
    | 'fetchTable'
    | 'addRecords'
    | 'deleteRecords'
    | 'updateRecords'
    | 'syncTable'
  > {
  fetchTable<TableName extends keyof Tables>(
    tableName: TableName,
    filters?: FilterSpec<Tables[TableName]>
  ): Promise<Tables[TableName][]>
  addRecords<TableName extends keyof Tables>(
    tableName: TableName,
    records: Partial<Tables[TableName]>[]
  ): Promise<number[]>
  deleteRecords<TableName extends keyof Tables>(
    tableName: TableName,
    recordIds: number[]
  ): Promise<void>
  updateRecords<TableName extends keyof Tables>(
    tableName: TableName,
    records: (Partial<Tables[TableName]> & { id: number })[]
  ): Promise<void>
  syncTable<TableName extends keyof Tables>(
    tableName: TableName,
    records: Partial<Tables[TableName]>[],
    keyColIds: (keyof Tables[TableName])[],
    options?: { filters?: FilterSpec<Tables[TableName]> }
  ): Promise<void>
}
export type AnyTable = { [colId: string]: unknown }
export type AnyTables = { [table: string]: AnyTable }
export type FilterSpec<Table extends AnyTable> = {
  [ColId in keyof Table]?: Table[ColId][]
}


================================================
FILE: core/src/bot.ts
================================================
import Encrypted from '@dtinth/encrypted'
import { MessageEvent, middleware, WebhookEvent } from '@line/bot-sdk'
import axios from 'axios'
import cors from 'cors'
import express, {
  NextFunction,
  Request,
  RequestHandler,
  Response,
} from 'express'
import handler from 'express-async-handler'
import { claimEquals, auth as jwtAuth } from 'express-oauth2-jwt-bearer'
import { Stream } from 'stream'
import sealedbox from 'tweetnacl-sealedbox-js'
import { getPendingCronJobs, updateCronJob } from './Cron'
import { encrypt } from './DataEncryption'
import { checkDeviceOnlineStatus, trackDevice } from './DeviceTracking'
import { LINEClient } from './LINEClient'
import { toMessages } from './LINEMessageUtilities'
import { logger } from './logger'
import { handleImage, handleTextMessage } from './MessageHandler'
import { getMessageHistory } from './MessageHistory'
import { handleNotification } from './NotificationProcessor'
import { ref } from './PersistentState'
import { deployPrelude } from './PreludeCode'
import { createErrorMessage, SlackMessage } from './SlackMessageUtilities'
import { handleSMS } from './SMSHandler'
import { getAllSpeedDials } from './SpeedDial'
import { AutomatronContext } from './types'

const app = express()
app.set('trust proxy', true)

function getAutomatronContext(req: Request, res: Response): AutomatronContext {
  return {
    secrets: req.env,
    tracer: req.tracer,
    requestInfo: {
      ip: req.ip || '',
      ips: req.ips,
      headers: req.headers,
    },
    addPromise: (name, promise) => {
      if (!res.yields) res.yields = []
      res.yields.push(promise)
    },
  }
}

async function runMiddleware(
  req: Request,
  res: Response,
  middleware: RequestHandler
): Promise<void> {
  return new Promise((resolve, reject) => {
    middleware(req, res, (error) => {
      if (error) {
        reject(error)
      } else {
        resolve()
      }
    })
  })
}

async function handleWebhook(
  context: AutomatronContext,
  events: WebhookEvent[],
  client: LINEClient
) {
  async function main() {
    for (const event of events) {
      if (event.type === 'message') {
        await handleMessageEvent(event)
      }
    }
  }

  async function handleMessageEvent(event: MessageEvent) {
    const { replyToken, message } = event
    if (event.source.userId !== context.secrets.LINE_USER_ID) {
      await client.replyMessage(replyToken, toMessages('unauthorized'))
      return
    }
    client.showLoadingAnimation(event.source.userId).catch((e) => {
      logger.error({ err: e }, 'Unable to show loading animation')
    })
    if (message.type === 'text') {
      const reply = await handleTextMessage(context, message.text, {
        source: 'line',
      })
      await client.replyMessage(replyToken, toMessages(reply))
    } else if (message.type === 'sticker') {
      const reply = await handleTextMessage(
        context,
        'sticker:' + message.packageId + ':' + message.stickerId,
        { source: 'line' }
      )
      await client.replyMessage(replyToken, toMessages(reply))
    } else if (message.type === 'image') {
      const content = await client.getMessageContent(message.id)
      const buffer = await readAsBuffer(content)
      const reply = await handleImage(context, buffer as Buffer, {
        source: 'line',
      })
      await client.replyMessage(replyToken, toMessages(reply))
    } else {
      await client.replyMessage(replyToken, [
        { type: 'text', text: 'don’t know how to handle this yet!' },
      ])
    }
  }

  return main()
}

app.post(
  '/webhook',
  handler(async (req, res) => {
    const lineConfig = getLineConfig(req, res)
    await runMiddleware(req, res, middleware(lineConfig))
    await handleRequest(req, res, async (context, services) => {
      const lineClient = services.line
      logger.info(
        { ingest: 'line', event: JSON.stringify(req.body) },
        'Received webhook from LINE'
      )
      const data = await handleWebhook(context, req.body.events, lineClient)
      return data
    })
  })
)

app.post(
  '/slack',
  require('body-parser').json(),
  (req, res, next) => {
    logger.info(
      { ingest: 'slack', event: JSON.stringify(req.body) },
      'Received an event from Slack'
    )
    if (req.body.type === 'url_verification') {
      res.set('Content-Type', 'text/plain').send(req.body.challenge)
      return
    }
    next()
  },
  endpoint(async (context, req, services) => {
    if (req.body.type === 'event_callback') {
      let globalScope = global as unknown as {
        automatronSlackEventCache: Set<string>
      }
      let eventCache = globalScope.automatronSlackEventCache
      if (!eventCache) {
        eventCache = new Set()
        globalScope.automatronSlackEventCache = eventCache
      }
      const eventId = req.body.event_id
      if (eventCache.has(eventId)) {
        return
      }
      eventCache.add(eventId)
      if (req.body.event.user === req.env.SLACK_USER_ID) {
        const text = String(req.body.event.text)
          .replace(/&gt;/g, '>')
          .replace(/&lt;/g, '>')
          .replace(/&amp;/g, '&')
        const slackClient = services.slack
        const reply = await handleTextMessage(context, text, {
          source: 'slack',
        })
        await slackClient.pushMessage({
          text: `\`\`\`${JSON.stringify(reply, null, 2)}\`\`\``,
        })
      }
    }

    return 1
  })
)

app.post(
  '/post',
  require('body-parser').json(),
  requireApiKey,
  endpoint(async (context, req, services) => {
    const lineClient = services.line
    const messages = toMessages(req.body.data)
    await lineClient.pushMessage(context.secrets.LINE_USER_ID, messages)
  })
)

app.post(
  '/text',
  require('body-parser').json(),
  requireApiKey,
  endpoint(async (context, req, services) => {
    logger.info(
      { ingest: 'text', event: JSON.stringify(req.body) },
      'Received a text API call'
    )
    const text = String(req.body.text)
    logToSlack(context, services.auditSlack, text, req.body.source)
    const reply = await handleTextMessage(context, text, {
      source: 'text:' + req.body.source,
    })
    return reply
  })
)

app.options('/webpost-firebase', cors() as any)
app.post(
  '/webpost-firebase',
  require('body-parser').json(),
  requireFirebaseAuth,
  cors(),
  endpoint(async (context, req, services) => {
    logger.info(
      { ingest: 'webpost-firebase', event: JSON.stringify(req.body) },
      'Received a webpost API call with Firebase credentials'
    )
    const text = String(req.body.text)
    logToSlack(context, services.auditSlack, text, req.body.source)
    const reply = await handleTextMessage(context, text, {
      source: 'webpost:' + req.body.source,
    })
    return reply
  })
)

app.options('/history', cors() as any)
app.get(
  '/history',
  requireFirebaseAuth,
  cors(),
  endpoint(async (context, req) => {
    logger.info(
      { ingest: 'history', event: JSON.stringify(req.body) },
      'Received a history API call'
    )
    return {
      history: await getMessageHistory(context, { limit: 20 }),
    }
  })
)

app.options('/speed-dials', cors() as any)
app.get(
  '/speed-dials',
  requireFirebaseAuth,
  cors(),
  endpoint(async (context, req) => {
    logger.info(
      { ingest: 'speed-dials', event: JSON.stringify(req.body) },
      'Received a speed dial API call'
    )
    return {
      speedDials: await getAllSpeedDials(context),
    }
  })
)

app.options('/knobs', cors() as any)
app.get(
  '/knobs',
  requireFirebaseAuth,
  cors(),
  endpoint(async (context, req) => {
    logger.info({ ingest: 'knobs' }, 'Received a knobs API call')
    const knobKeys = ((await ref(context, 'knobs').get()) as string).split(',')
    const knobs = Object.fromEntries(
      await Promise.all(
        knobKeys.map(async (key) => {
          const value = await ref(context, key).get()
          return [key, value] as [string, string]
        })
      )
    )
    return { knobs }
  })
)

// http post $AUTOMATRON/encrypt data=meow
app.post(
  '/encrypt',
  express.json(),
  endpoint(async (context, req) => {
    return encrypt(context, req.body.data)
  })
)

function logToSlack(
  context: AutomatronContext,
  slack: Slack,
  text: string,
  source: string
) {
  context.addPromise(
    'Log to Slack',
    slack.pushMessage({
      blocks: [
        {
          type: 'context',
          elements: [{ type: 'plain_text', text: 'from ' + source }],
        },
        {
          type: 'section',
          text: { type: 'plain_text', text: text },
        },
      ],
    })
  )
}

app.post(
  '/gh/prelude/push',
  require('body-parser').json(),
  endpoint(async (context, req, services) => {
    logger.info(
      { ingest: 'prelude-push', event: JSON.stringify(req.body) },
      'Received prelude push webhook from GitHub'
    )
    await deployPrelude(context)
    return 'ok'
  })
)

app.post(
  '/sms',
  require('body-parser').json(),
  requireApiKey,
  endpoint(async (context, req, services) => {
    const text = String(req.body.text)
    return await handleSMS(context, services.line, text)
  })
)

app.post(
  '/notification',
  require('body-parser').text(),
  endpoint(async (context, req, services) => {
    try {
      const encrypted = Encrypted(context.secrets.ENCRYPTION_SECRET)
      const { publicKey, secretKey } = encrypted(`
        BpnjVPJcwkInfE/lPxxcl6E11BlDNh3v.KoCet77F7KC4pvuhRySH2wNP1AjYpUGVcHQSqhc
        rbFTDHUsXDaYjF/Jc584uh7Bd6yLl0a4scdEsX7EhxuHXUknD4bA8AXxkJe/OhI3EbmfleP5
        ByVNvvvxqScM9pHvCy/bURK33REznhvW0MsscwgRGsqMxvI7Km9RpxpglexWANMlrkuVBJbC
        G3CeOqs9QGI3QS0K+jse8PM7HvJ8vg43AAjQsx6o85xSzaGWVWE1wdNtWfkdusGf/NYbDyb6
        hgA9ddrCRJVMydqJ4g9A/LgpieO0v
      `)
      const result = sealedbox.open(
        Buffer.from(req.body, 'hex'),
        Buffer.from(publicKey, 'base64'),
        Buffer.from(secretKey, 'base64')
      )
      const notification = JSON.parse(Buffer.from(result).toString('utf8'))
      logger.info(
        { ingest: 'notification', notification },
        'Received a notification from ' + notification.packageName
      )
      const deviceId = String(req.query.deviceId ?? 'phone')
      await trackDevice(context, deviceId, {})
      await handleNotification(context, notification)
    } catch (err) {
      logger.error({ err, data: req.body }, 'Unable to process notification')
    }
  })
)

app.get(
  '/cron',
  endpoint(async (context, req, services) => {
    const otherTasks: Promise<any>[] = []
    otherTasks.push(
      (async () => {
        await checkDeviceOnlineStatus(context)
      })()
    )
    await Promise.all(otherTasks)

    const pendingJobs = await getPendingCronJobs(context)
    const jobsToRun = pendingJobs.filter(
      (j) => new Date().toISOString() >= j.scheduledTime
    )
    logger.trace('Number of pending cron jobs found: %s', jobsToRun.length)
    try {
      for (const job of jobsToRun) {
        let result = 'No output'
        const logContext = {
          job: { id: job._id.toString(), name: job.name },
        }
        try {
          const reply = await handleTextMessage(context, job.name, {
            source: 'cron:' + job._id.toString(),
          })
          result = require('util').inspect(reply)
          logger.info(
            { ...logContext, result },
            `Done processing cron job: ${job.name}`
          )
        } catch (e) {
          logError('Unable to process cron job', e, logContext)
          result = `Error: ${e}`
        }
        await updateCronJob(context, job._id.toString(), {
          completed: true,
          notes: result,
        })
      }
      return 'All OK'
    } catch (e) {
      logError('Unable to process cron jobs', e)
      return 'Error: ' + e
    }
  })
)

app.post(
  '/mac/ping',
  require('body-parser').json(),
  requireApiKey,
  endpoint(async (context, req, services) => {
    const deviceId = req.body.deviceId
    if (!deviceId) {
      throw new Error('Missing deviceId')
    }
    const newState = await trackDevice(context, deviceId, {
      locked: req.body.locked || null,
      powerSource: req.body.powerSource,
      online: true,
    })
    return { newState }
  })
)

function requireApiKey(req: Request, res: Response, next: NextFunction) {
  const context = getAutomatronContext(req, res)
  if (req.body.key !== context.secrets.API_KEY) {
    return res.status(401).json({ error: 'Invalid API key' })
  }
  next()
}

const firebaseAuthn = jwtAuth({
  issuer: 'https://securetoken.google.com/dtinth-automatron',
  jwksUri:
    'https://www.googleapis.com/service_accounts/v1/jwk/securetoken@system.gserviceaccount.com',
  audience: 'dtinth-automatron',
})
function requireFirebaseAuth(req: Request, res: Response, next: NextFunction) {
  const encrypted = Encrypted(req.env.ENCRYPTION_SECRET)
  const sub = encrypted(
    `HnX5zHMDR/F7ZUy8sw2829Wi7cYbi/3c.6CBZ+I3sTbyYsXUg81qf+OvjRQExIfVkkhGvEs9E+w7LAO4y9KwU1DtKAtIutA==`
  )
  return firebaseAuthn(req, res, (err) => {
    if (err) {
      return next(err)
    }
    claimEquals('sub', sub)(req, res, next)
  })
}

class Slack {
  constructor(private webhookUrl: string) {}
  async pushMessage(message: SlackMessage) {
    await axios.post(this.webhookUrl, message)
  }
}

interface ThirdPartyServices {
  line: LINEClient
  slack: Slack
  auditSlack: Slack
}

function endpoint(
  f: (
    context: AutomatronContext,
    req: Request,
    services: ThirdPartyServices
  ) => Promise<any>
): RequestHandler {
  return handler(async (req, res) => {
    await handleRequest(req, res, (context, services) =>
      f(context, req, services)
    )
  })
}

async function handleRequest(
  req: Request,
  res: Response,
  f: (context: AutomatronContext, services: ThirdPartyServices) => Promise<any>
) {
  const context = getAutomatronContext(req, res)
  const encrypted = Encrypted(context.secrets.ENCRYPTION_SECRET)
  const lineConfig = getLineConfig(req, res)
  const lineClient = new LINEClient(lineConfig)
  const slackClient = new Slack(context.secrets.SLACK_WEBHOOK_URL)
  const auditSlackClient = new Slack(
    encrypted(
      'j3o0uDUL3OuYfUsYxZUkI8ECdaUIGxW0.HX1CMjS27oaZHnQormJbPIoE9xdPB3GsITBVXW2oIFeuuAb4xyWVJZyywWMubR1I1ECkXtBJN+Fs+98MECYk+u9YnlnAgw6DlE9e8TezE88C5DeNtOO0DOnSO6ww39Cn/w=='
    )
  )
  try {
    const result = await f(context, {
      line: lineClient,
      slack: slackClient,
      auditSlack: auditSlackClient,
    })
    await Promise.allSettled(res.yields || [])
    res.json({ ok: true, result })
  } catch (e) {
    logError('Unable to execute endpoint ' + req.path, e)
    try {
      await slackClient.pushMessage(createErrorMessage(e as any))
    } catch (ee) {
      console.error('Cannot send error message to LINE!')
      logError('Unable to send error message to Slack', ee)
    }
    await Promise.allSettled(res.yields || [])
    throw e
  }
}

function logError(title: string, e: any, extra: Record<string, any> = {}) {
  var response = e.response || (e.originalError && e.originalError.response)
  var data = response && response.data
  const bindings: Record<string, any> = { ...extra, err: e }
  if (data) {
    bindings.responseData = JSON.stringify(data)
  }
  logger.error(bindings, `${title}: ${e}`)
}

function getLineConfig(req: Request, res: Response) {
  const context = getAutomatronContext(req, res)
  const isDev = process.env.AUTOMATRON_ENV === 'dev'
  return {
    channelAccessToken: isDev
      ? context.secrets.DEV_LINE_CHANNEL_ACCESS_TOKEN
      : context.secrets.LINE_CHANNEL_ACCESS_TOKEN,
    channelSecret: isDev
      ? context.secrets.DEV_LINE_CHANNEL_SECRET
      : context.secrets.LINE_CHANNEL_SECRET,
  }
}

function readAsBuffer(stream: Stream) {
  return new Promise((resolve, reject) => {
    stream.on('error', (e: Error) => {
      reject(e)
    })
    const bufs: Buffer[] = []
    stream.on('end', () => {
      resolve(Buffer.concat(bufs))
    })
    stream.on('data', (buf: Buffer) => {
      bufs.push(buf)
    })
  })
}

module.exports = app


================================================
FILE: core/src/logger.ts
================================================
import pino from 'pino'

export const logger = pino({
  // https://github.com/pinojs/pino/issues/726#issuecomment-605814879
  messageKey: 'message',
  formatters: {
    level: (label) => {
      function getSeverity(label: string) {
        switch (label) {
          case 'trace':
            return 'DEBUG'
          case 'debug':
            return 'DEBUG'
          case 'info':
            return 'INFO'
          case 'warn':
            return 'WARNING'
          case 'error':
            return 'ERROR'
          case 'fatal':
            return 'CRITICAL'
          default:
            return 'DEFAULT'
        }
      }
      return { severity: getSeverity(label) }
    },
  },
}).child({ name: 'automatron' })


================================================
FILE: core/src/modules.d.ts
================================================
declare module '@google-cloud/vision' {
  export class ImageAnnotatorClient {
    constructor(options?: any)
    documentTextDetection(
      imageBuffer: Buffer
    ): Promise<
      {
        fullTextAnnotation: {
          pages: {
            blocks: {
              paragraphs: {
                words: {
                  symbols: {
                    text: string
                  }[]
                }[]
              }[]
            }[]
          }[]
        }
      }[]
    >
  }
}

declare module 'airtable' {
  export interface AirtableRecord {
    get(field: string): any
    getId(): string
  }
  export default class Airtable {
    constructor(options: any)
    base(
      id: string
    ): {
      table(
        name: string
      ): {
        select(
          options?: any
        ): {
          all(): Promise<AirtableRecord[]>
        }
        create(data: any, options?: any): Promise<AirtableRecord>
        update(id: string, data: any): Promise<AirtableRecord>
      }
    }
  }
}


================================================
FILE: core/src/scripts/updateEnv.ts
================================================
import { Storage } from '@google-cloud/storage'
import { password } from '@inquirer/prompts'
import * as age from 'age-encryption'
import { defineCommand, runMain } from 'citty'

const main = defineCommand({
  meta: {
    name: 'updateEnv',
    description:
      'Encrypt and upload environment variables to Google Cloud Storage',
  },
  args: {
    name: {
      type: 'positional',
      description: 'Name',
      required: true,
    },
  },
  async run({ args }) {
    const name = args.name
    const value = await password({
      message: `Enter value for ${name}:`,
      mask: true,
    })
    const e = new age.Encrypter()
    e.addRecipient(
      'age1234vw9hka4p6eurluezk49kn46ywqpshywy6eypvs0kw0dep6a4sn6gyaz'
    )
    e.addRecipient(
      'age1eeezwmr0u6pnl2rgw56dl3k5pcsmxfmzv42varg2zzm96a7s64as9cffpl'
    )
    const ciphertext = await e.encrypt(value)
    const encoded = age.armor.encode(ciphertext)
    console.log(name)
    console.log(encoded)

    await writeEnv(name, encoded)
  },
})

async function writeEnv(key: string, encodedValue: string) {
  const storage = new Storage()
  const [, bucket, file] = process.env.AUTOMATRON_ENV_GS_URI!.match(
    /^gs:\/\/([^\/]+)\/(.+)$/
  )!
  const [data] = await storage.bucket(bucket).file(file).download()
  const encryptedEnv = JSON.parse(data.toString()) as Record<string, string>
  encryptedEnv[key] = encodedValue
  await storage
    .bucket(bucket)
    .file(file)
    .save(JSON.stringify(encryptedEnv, null, 2))
  console.log(`Updated ${key} in ${process.env.AUTOMATRON_ENV_GS_URI}`)
}

runMain(main)


================================================
FILE: core/src/server.ts
================================================
import { Storage } from '@google-cloud/storage'
import { start } from '@google-cloud/trace-agent'
import * as age from 'age-encryption'
import crypto from 'crypto'
import express from 'express'
import 'google-application-credentials-base64'
import { BotSecrets } from './BotSecrets'

const bot = require('./bot')
const tracer = start()

const app = express()
const PORT = process.env.PORT || 28364

const storage = new Storage()
const [, bucket, file] = process.env.AUTOMATRON_ENV_GS_URI!.match(
  /^gs:\/\/([^\/]+)\/(.+)$/
)!

function loadEnv(): Promise<BotSecrets> {
  return storage
    .bucket(bucket)
    .file(file)
    .download()
    .then(async ([data]) => {
      const encryptedEnv = JSON.parse(data.toString()) as Record<string, string>
      const env: Record<string, string> = {}
      const d = new age.Decrypter()
      d.addIdentity(process.env.AGE_SECRET_KEY!)
      for (const [key, value] of Object.entries(encryptedEnv)) {
        const ciphertext = age.armor.decode(value)
        env[key] = await d.decrypt(ciphertext, 'text')
      }
      console.log('Decrypted keys:', Object.keys(env))
      return env
    }) as Promise<BotSecrets>
}
let envPromise: Promise<BotSecrets> = loadEnv()
let reloadPromise: Promise<BotSecrets> | null = null
envPromise
  .then(async (env) => {
    console.log('Environment has been loaded')
  })
  .catch((err) => {
    console.error('Unable to load environment', err)
  })
app.post('/run/automatron/reload', async (req, res, next) => {
  try {
    let validationEnv: BotSecrets
    try {
      validationEnv = await envPromise
    } catch (err) {
      console.warn('Existing environment unavailable, attempting reload', err)
      validationEnv = await loadEnv()
    }
    const providedApiKey = req.get('X-API-Key')
    const providedBuffer = providedApiKey
      ? Buffer.from(providedApiKey, 'utf8')
      : undefined
    const expectedBuffer = Buffer.from(validationEnv.API_KEY, 'utf8')
    if (
      !providedBuffer ||
      providedBuffer.length !== expectedBuffer.length ||
      !crypto.timingSafeEqual(providedBuffer, expectedBuffer)
    ) {
      res.status(401).send('Invalid API key')
      return
    }
    const startReload = () => {
      const nextEnvPromise = loadEnv()
      return nextEnvPromise
        .then((env) => {
          envPromise = nextEnvPromise
          return env
        })
        .catch((err) => {
          console.error('Unable to reload environment', err)
          throw err
        })
        .finally(() => {
          reloadPromise = null
        })
    }
    if (!reloadPromise) {
      reloadPromise = startReload()
    }
    await reloadPromise
    res.json({ ok: true })
  } catch (err) {
    next(err)
  }
})
app.use('/run/automatron', async (req, res, next) => {
  try {
    req.tracer = tracer
    req.env = (await envPromise) as unknown as BotSecrets
    bot(req, res)
  } catch (err) {
    next(err)
  }
})
app.get('/', (req, res) => {
  res.send('Automatron is running')
})
app.listen(PORT, () => {
  console.log(`Automatron server is running on port ${PORT}`)
})


================================================
FILE: core/src/typedefs.d.ts
================================================
declare module 'tweetnacl-sealedbox-js'
declare module 'lib'


================================================
FILE: core/src/types.ts
================================================
import { FlexMessage, messagingApi, TextMessage } from '@line/bot-sdk'
import { BotSecrets } from './BotSecrets'
import type { PluginTypes } from '@google-cloud/trace-agent'
import { IncomingHttpHeaders } from 'http'

export interface AutomatronContext {
  secrets: BotSecrets
  requestInfo: HttpRequestInfo
  tracer?: PluginTypes.Tracer
  addPromise: (name: string, promise: Promise<any>) => void
}

export interface HttpRequestInfo {
  ip: string
  ips: string[]
  headers: IncomingHttpHeaders
}

declare global {
  module Express {
    interface Request {
      env: BotSecrets
      tracer?: PluginTypes.Tracer
    }
    interface Response {
      yields?: Promise<any>[]
    }
  }
  module NodeJS {
    interface Global {
      automatronSlackEventCache?: Set<string>
    }
  }
}

export { FlexMessage, TextMessage } from '@line/bot-sdk'

export type AutomatronResponse =
  | string
  | messagingApi.TextMessage
  | messagingApi.TextMessage[]
  | messagingApi.FlexMessage
  | messagingApi.FlexMessage[]

export type TextMessageHandler = (
  text: string,
  context: AutomatronContext
) => (() => Promise<AutomatronResponse>) | undefined


================================================
FILE: core/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "noEmit": true,
    "lib": ["es2020"],
    "esModuleInterop": true,
    "strict": true,
    "rootDir": "src",
    "skipLibCheck": true
  }
}


================================================
FILE: fnox.toml
================================================
default_provider = "age"

[providers.age]
type = "age"
recipients = [
  "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHac2M/WdCsejGpA87JlU2uwvVokPhqD7eufdMDoEU8z",
  "age1234vw9hka4p6eurluezk49kn46ywqpshywy6eypvs0kw0dep6a4sn6gyaz",
]

[secrets]
AUTOMATRON_ENV_GS_URI = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IDU1NTU1QSBuUEs0cnFxTmowMWVGNFQ1WG41NUVIKzdoUHNodjFReW0wQW9LUFNIa2hNCnZEVFNWQ1FkYmhaaGR4cnhaNnBKQTFJcmlIblo3TWxlQTRKYTVyWHBRUjgKLT4gWDI1NTE5IHhYL3dOY0hUWWNwQzV1WTg5ejM2QUdKNTdUVVNUWVJEZlFlZk8wbU9GeWsKMS96TUdIWXpxbVJOOW5JQStRSnRWMHJJeEFYQmpmeXl5UnNFU0cvUmJrSQotLS0gWmErQlZXM1J3L2FCY1JTcHExZTVYWW1aN2xvV0FaT1FNTXZ1QXRvZWR5awoMoFXvpKR5HptiKYIwCNP1Nc2QygS/o6S1c3PY6V5kdAxAku0plOQMAef1PNR/ZZOUUdcAh8Eux38Yw+Cf6ARWPXSIewIt/csh" }
AGE_SECRET_KEY = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IDU1NTU1QSBmTjZmKy9lMzVmZDdjanE2NGttZHVtMTR0VEVQVHRMVVRTYmNLWC9DTVZZCjd2NHo5ZU5wd251NCtNd0cvNTREa3VtaCs3eXA4R2VuOVdsSUhueE1LZUEKLT4gWDI1NTE5IERleis4WFB6K2lLRC8wdjlVdTkxTDlxa01wb1BkMjJtOWc1aTU4ajY0SGcKVlpMbkRNZlZKNXFYRW9IQlBzVXl0OGlUYndCdVJMbTBzYWdMNWRlQWtrdwotLS0ga1RNaDVlVm5zUXc3cFF4UTVCMDhqS3ZjTk13bnFtd0xWcmJNeGxOSHhKMArxpe14F0CnSZsC3ULOoT+Ux+4LCbzE91ocIW15MnN6S6ltSKz7R22qN5gp5THqgb5D4wWiwgAJly6Zbi4lRr7QEXHEztHfMxKZbteRIQyFzXhpE0cijc7r9tz8fl2ZOjbBJzwe2Pxbl7b/" }
FRP_SERVER_ADDR = { provider = "age", value = "YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IDU1NTU1QSBITld6M2xlMS8ya1lyTW9UZEdnWVJZMnE1RDZnMzBseVEvdHZ1Y3NJeHk4Ck03c0YzOGRvQmlUckdJVk9td3V5WkxnZG93QnlKRlNtWjRiU3dZK243NzgKLT4gWDI1NTE5IDg4L2tURkdLN250VThHQVNBTUFqeHpHNG5GVDFDdGlNemZYQ3YzaGozM0EKVjU0TUJYSHVSSEdzMUxjbmdOY2srT0dBTGxuanhweGJCaWQ2MDdiSTlLRQotLS0gZkVkVXZMSXd2YlMvbFI2a1l2VGQzcmgrbHBjS0wzL3hXbXpHTzVzRWNlQQo2o4ALYA0LvVvDchUo6X6Jg6uoIlAVsT7vI9BFsWER2q8d0XQn9Kp7TzCMYl8=" }


================================================
FILE: frpc.toml
================================================
serverAddr = "{{ .Envs.FRP_SERVER_ADDR }}"
serverPort = 7000

[[proxies]]
name = "automatron"
type = "http"
localPort = 28364
subdomain = "automatron-tunnel"


================================================
FILE: images/.gitkeep
================================================



================================================
FILE: mise.toml
================================================
[tools]
fnox = "latest"
node = "24"

[env]
_.file = '.env'


================================================
FILE: package.json
================================================
{
  "name": "automatron-workspace",
  "private": true,
  "packageManager": "pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224ea77a81a5e8859cbc4e06b1511ddce2f0151512431dd19e6aff31f2c6a8f5f2aced9bd2273e1fed7dd4de1868984059d2c4247",
  "engines": {
    "node": "24.x"
  },
  "scripts": {
    "dev": "fnox exec -- pnpm -C core run dev",
    "tunnel": "fnox exec -- frpc -c frpc.toml",
    "env:set": "fnox exec -- pnpm -C core exec tsx src/scripts/updateEnv.ts"
  }
}


================================================
FILE: pnpm-workspace.yaml
================================================
packages:
  - core
  - webui


================================================
FILE: webui/.eslintrc.cjs
================================================
/**
 * This is intended to be a basic starting point for linting in your app.
 * It relies on recommended configs out of the box for simplicity, but you can
 * and should modify this configuration to best suit your team's needs.
 */

/** @type {import('eslint').Linter.Config} */
module.exports = {
  root: true,
  parserOptions: {
    ecmaVersion: 'latest',
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  env: {
    browser: true,
    commonjs: true,
    es6: true,
  },

  // Base config
  extends: ['eslint:recommended'],

  overrides: [
    // React
    {
      files: ['**/*.{js,jsx,ts,tsx}'],
      plugins: ['react', 'jsx-a11y'],
      extends: [
        'plugin:react/recommended',
        'plugin:react/jsx-runtime',
        'plugin:react-hooks/recommended',
        'plugin:jsx-a11y/recommended',
      ],
      settings: {
        react: {
          version: 'detect',
        },
        formComponents: ['Form'],
        linkComponents: [
          { name: 'Link', linkAttribute: 'to' },
          { name: 'NavLink', linkAttribute: 'to' },
        ],
        'import/resolver': {
          typescript: {},
        },
      },
      rules: {
        'jsx-a11y/no-autofocus': 'off',
      },
    },

    // Typescript
    {
      files: ['**/*.{ts,tsx}'],
      plugins: ['@typescript-eslint', 'import'],
      parser: '@typescript-eslint/parser',
      settings: {
        'import/internal-regex': '^~/',
        'import/resolver': {
          node: {
            extensions: ['.ts', '.tsx'],
          },
          typescript: {
            alwaysTryTypes: true,
          },
        },
      },
      extends: [
        'plugin:@typescript-eslint/recommended',
        'plugin:import/recommended',
        'plugin:import/typescript',
      ],
      rules: {
        '@typescript-eslint/no-unused-vars': 'off',
        'import/no-unresolved': 'off',
        '@typescript-eslint/no-explicit-any': 'off',
      },
    },

    // Node
    {
      files: ['.eslintrc.js'],
      env: {
        node: true,
      },
    },
  ],
}


================================================
FILE: webui/.gitignore
================================================
node_modules

/.cache
/build
.env
/dist/

================================================
FILE: webui/README.md
================================================
# Welcome to Remix + Vite!

📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite) for details on supported features.

## Development

Run the Vite dev server:

```shellscript
npm run dev
```

## Deployment

First, build your app for production:

```sh
npm run build
```

Then run the app in production mode:

```sh
npm start
```

Now you'll need to pick a host to deploy it to.

### DIY

If you're familiar with deploying Node applications, the built-in Remix app server is production-ready.

Make sure to deploy the output of `npm run build`

- `build/server`
- `build/client`


================================================
FILE: webui/app/Clock.tsx
================================================
import { useEffect, useState } from 'react'

export function Clock() {
  const [time, setTime] = useState(getTime)
  useEffect(() => {
    const interval = setInterval(() => {
      setTime(getTime())
    }, 1000)
    return () => clearInterval(interval)
  }, [])
  return (
    <div className="come-up flex h-screen items-center justify-center text-transparent [font:bold_37.5vw_Arimo] [-webkit-text-stroke:0.5px_#d7fc70]">
      {time}
    </div>
  )
}
const getTime = () => {
  return new Date().toString().split(' ')[4].split(':').slice(0, 2).join(':')
}


================================================
FILE: webui/app/backend.ts
================================================
import {
  getAuth,
  onAuthStateChanged,
  User,
  GoogleAuthProvider,
  signInWithPopup,
  signOut as signOutFirebase,
} from 'firebase/auth'
import { getFirestore, doc, getDoc } from 'firebase/firestore'
import { app } from './firebase'
import { SyncExternalStore } from 'sync-external-store'
import axios from 'axios'

const auth = getAuth(app)
const firestore = getFirestore(app)

class AutomatronBackend implements Backend {
  authReadyStatePromise: Promise<void>
  authStore = new SyncExternalStore<User | null | undefined>(undefined)
  private url?: string

  constructor() {
    this.authReadyStatePromise = new Promise<void>((resolve) => {
      onAuthStateChanged(auth, (user) => {
        this.authStore.state = user
        resolve()
        if (user && !this.url) {
          this.getUrl()
        }
      })
    })
  }

  private async getUrl() {
    if (this.url) {
      return this.url
    }
    const s = await getDoc(
      doc(firestore, 'apps', 'automatron', 'config', 'url')
    )
    this.url = s.data()?.service
    return this.url
  }

  async signIn() {
    try {
      const provider = new GoogleAuthProvider()
      await signInWithPopup(auth, provider)
    } catch (error) {
      console.error(error)
      alert(`Unable to sign in: ${error}`)
    }
  }

  async signOut() {
    await signOutFirebase(auth)
  }

  async send(text: string): Promise<any> {
    const data = await this._post('/webpost-firebase', { text, source: 'web' })
    if (typeof data.result === 'string') {
      data.result = [
        {
          type: 'text',
          text: data.result,
        },
      ]
    }
    return data
  }

  private async getHeaders() {
    return {
      Authorization: `Bearer ${await this.getIdToken()}`,
    }
  }

  async getHistory(): Promise<any> {
    return this._get('/history')
  }

  async getSpeedDials(): Promise<any> {
    return this._get('/speed-dials')
  }

  async getKnobs(): Promise<Ok<{ knobs: Record<string, string> }>> {
    return this._get('/knobs')
  }

  private async getIdToken() {
    return await auth.currentUser!.getIdToken()
  }

  async _get(url: string) {
    const response = await axios.get((await this.getUrl()) + url, {
      headers: await this.getHeaders(),
    })
    return response.data
  }

  async _post(url: string, data: any) {
    const response = await axios.post((await this.getUrl()) + url, data, {
      headers: await this.getHeaders(),
    })
    return response.data
  }
}

class FakeBackend implements Backend {
  authReadyStatePromise: Promise<void> = Promise.resolve()
  authStore = new SyncExternalStore<User | null | undefined>(null)

  async signIn() {
    this.authStore.state = {} as User
  }

  async signOut() {
    this.authStore.state = null
  }

  async getHistory(): Promise<any> {
    return { ok: true, result: { history: [] } }
  }

  async getSpeedDials(): Promise<any> {
    return { ok: true, result: { speedDials: [] } }
  }

  async getKnobs(): Promise<Ok<{ knobs: Record<string, string> }>> {
    return { ok: true, result: { knobs: {} } }
  }

  async send(text: string): Promise<any> {
    return JSON.parse(text)
  }
}

type Ok<X> = { ok: true; result: X }

interface Backend {
  authReadyStatePromise: Promise<void>
  authStore: SyncExternalStore<User | null | undefined>
  signIn(): Promise<void>
  signOut(): Promise<void>
  send(text: string): Promise<any>
  getHistory(): Promise<any>
  getSpeedDials(): Promise<any>
  getKnobs(): Promise<Ok<{ knobs: Record<string, string> }>>
}

export const backend =
  typeof location === 'undefined' ||
  new URLSearchParams(location.search).get('backend') === 'fake'
    ? new FakeBackend()
    : new AutomatronBackend()

if (typeof window !== 'undefined') {
  Object.assign(window, { backend })
}


================================================
FILE: webui/app/firebase.ts
================================================
import { initializeApp } from 'firebase/app'

// Your web app's Firebase configuration
const firebaseConfig = {
  apiKey: 'AIzaSyAmj0axCEWY5ojSMDs25h0hNNfMFCRIqys',
  authDomain: 'dtinth-automatron.firebaseapp.com',
  projectId: 'dtinth-automatron',
  storageBucket: 'dtinth-automatron.appspot.com',
  messagingSenderId: '347735770628',
  appId: '1:347735770628:web:8fce6a02b34c2d17c2d751',
}

// Initialize Firebase
export const app = initializeApp(firebaseConfig)


================================================
FILE: webui/app/index.css
================================================
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
  font-family: Arimo, Helvetica, Arial, sans-serif;
}

.come-up {
  animation: 0.25s come-up;
}

@keyframes come-up {
  from {
    transform: scale(0.99);
    opacity: 0;
  }
  to {
    transform: scale(1);
    opacity: 1;
  }
}

body::-webkit-scrollbar {
  display: none;
}

.bg-bevel {
  background: #252423 linear-gradient(to bottom, #454443, #151413);
}

.bg-emboss {
  background: #252423 linear-gradient(to bottom, #151413, #292827);
}

.bg-glossy {
  background: #252423
    linear-gradient(
      to bottom,
      #353433 0%,
      #252423 50%,
      #151413 50%,
      #252423 100%
    );
}


================================================
FILE: webui/app/requireAuth.ts
================================================
import { backend } from './backend'
import { redirect } from '@remix-run/react'

export async function requireAuth() {
  await backend.authReadyStatePromise
  if (!backend.authStore.state) {
    throw redirect('/')
  }
}


================================================
FILE: webui/app/root.tsx
================================================
import {
  Links,
  LiveReload,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from '@remix-run/react'

import './index.css'

export default function App() {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body className="bg-#353433 text-white">
        <Outlet />
        <ScrollRestoration />
        <Scripts />
        <LiveReload />
      </body>
    </html>
  )
}


================================================
FILE: webui/app/routes/_index.tsx
================================================
import { ReactNode, useSyncExternalStore } from 'react'
import { Clock } from '~/Clock'
import { backend } from '~/backend'
import { Icon } from '@iconify-icon/react'
import running from '@iconify-icons/cil/running'
import menu from '@iconify-icons/cil/menu'
import { useNavigate } from '@remix-run/react'

export default function Index() {
  return (
    <>
      <AuthButton />
      <Clock />
    </>
  )
}

function AuthButton() {
  const authState = useSyncExternalStore(
    backend.authStore.subscribe,
    backend.authStore.getSnapshot
  )
  const navigate = useNavigate()

  return (
    <>
      {authState === undefined ? (
        <></>
      ) : authState === null ? (
        <FloatingButton onClick={() => backend.signIn()} title="Sign In">
          <Icon icon={running} />
        </FloatingButton>
      ) : (
        <FloatingButton
          onClick={() => {
            navigate('/automatron')
          }}
          title="Automatron"
        >
          <Icon icon={menu} />
        </FloatingButton>
      )}
      <Clock />
    </>
  )
}

export interface FloatingButton {
  title: string
  children: ReactNode
  onClick?: () => void
}

export function FloatingButton(props: FloatingButton) {
  return (
    <div className="absolute top-2 right-2">
      <button
        onClick={props.onClick}
        className="p-2 text-3xl text-#8b8685"
        title={props.title}
      >
        {props.children}
      </button>
    </div>
  )
}


================================================
FILE: webui/app/routes/automatron._index.tsx
================================================
import { Icon } from '@iconify-icon/react'
import chevronLeft from '@iconify-icons/cil/chevron-left'
import chevronRight from '@iconify-icons/cil/chevron-right'
import history from '@iconify-icons/cil/history'
import send from '@iconify-icons/cil/send'
import {
  Await,
  ClientActionFunctionArgs,
  Form,
  useActionData,
  useLoaderData,
  useNavigation,
} from '@remix-run/react'
import clsx from 'clsx'
import { ReactNode, Suspense, useState } from 'react'
import { z } from 'zod'
import { backend } from '~/backend'
import { requireAuth } from '~/requireAuth'

export const clientLoader = async () => {
  await requireAuth()
  return {
    speedDialsPromise: backend.getSpeedDials(),
    historyPromise: backend.getHistory(),
  }
}

export interface ActionResult {
  result?: unknown
  error?: unknown
}

export const clientAction = async (
  args: ClientActionFunctionArgs
): Promise<ActionResult> => {
  await requireAuth()
  const form = await args.request.formData()
  const text = form.get('text')
  const result = await backend.send(String(text))
  try {
    return { result } as ActionResult
  } catch (error) {
    return { error }
  }
}

const classes = {
  button:
    'bg-bevel hover:border-#555453 block rounded border border-#454443 p-2 shadow-md shadow-black/50 active:border-#8b8685 flex flex-col items-center justify-center',
}

export default function AutomatronConsole() {
  const data = useLoaderData<typeof clientLoader>()
  const [speedDialEnabled, setSpeedDialEnabled] = useState(false)
  const [historyEnabled, setHistoryEnabled] = useState(false)
  const { error, result } = useActionData<typeof clientAction>() ?? {}
  const navigation = useNavigation()
  const isSubmitting = navigation.state === 'submitting'

  return (
    <>
      <Form className="flex gap-2" method="POST">
        <textarea
          name="text"
          className="bg-emboss hover:border-#555453 block w-full flex-1 rounded border border-#454443 py-1 px-2 font-mono placeholder-#8b8685 shadow-md shadow-black/50 focus:border-#656463 active:border-#8b8685"
          placeholder="Talk to automatron"
          autoFocus
        />
        <button
          className={clsx(classes.button, 'text-xl text-#8b8685')}
          type="submit"
          title="Send"
          disabled={isSubmitting}
        >
          <Icon icon={send} />
        </button>
      </Form>
      <div className="mt-2 flex gap-2">
        <button
          className={clsx(classes.button, 'text-lg text-#8b8685')}
          onClick={() => setHistoryEnabled((x) => !x)}
        >
          <Icon icon={history} />
        </button>
        <button
          className={clsx(classes.button, 'text-lg text-#8b8685')}
          onClick={() => setSpeedDialEnabled((x) => !x)}
        >
          <Icon icon={speedDialEnabled ? chevronLeft : chevronRight} />
        </button>
        {speedDialEnabled && (
          <Suspense fallback={<div className="self-center">Loading...</div>}>
            <Await resolve={data.speedDialsPromise}>
              {(speedDials) => <SpeedDial speedDialData={speedDials} />}
            </Await>
          </Suspense>
        )}
      </div>
      {historyEnabled && (
        <div className="mt-4">
          <Panel title="History">
            <Suspense fallback={<div>Loading...</div>}>
              <Await resolve={data.historyPromise}>
                {(historyData) => (
                  <AutomatronHistory historyData={historyData} />
                )}
              </Await>
            </Suspense>
          </Panel>
        </div>
      )}
      <div className="mt-4">
        {!!isSubmitting && (
          <pre className="whitespace-pre-wrap font-mono text-yellow-400">
            Sending...
          </pre>
        )}
        {!!error && (
          <pre className="whitespace-pre-wrap font-mono text-red-300">
            {String(error)}
          </pre>
        )}
        {!!result && <OutputViewer data={result} />}
      </div>
    </>
  )
}

export interface OutputViewer {
  data: unknown
}

type OkResult = z.infer<typeof OkResult>
const OkResult = z.object({
  ok: z.literal(true),
  result: z.array(
    z
      .object({
        type: z.string(),
      })
      .passthrough()
  ),
})

export function OutputViewer(props: OutputViewer) {
  const { data } = props
  const okResult = OkResult.safeParse(data)
  if (okResult.success) {
    const result = okResult.data.result
    return (
      <div className="flex flex-col gap-2 text-sm">
        {result.map((item, index) => (
          <ResultItem key={index} item={item as any} />
        ))}
      </div>
    )
  }
  return (
    <pre className="whitespace-pre-wrap font-mono">
      {typeof data === 'string' ? data : JSON.stringify(data, null, 2)}
    </pre>
  )
}

type ResultEntry = { type: 'text'; text: string }

export interface ResultItem {
  item: ResultEntry
}

export function ResultItem(props: ResultItem) {
  const { item } = props
  const children = (() => {
    switch (item.type) {
      case 'text':
        return <pre className="whitespace-pre-wrap font-mono">{item.text}</pre>
      default:
        return (
          <pre className="whitespace-pre-wrap font-mono">
            {JSON.stringify(item, null, 2)}
          </pre>
        )
    }
  })()
  return (
    <div className="rounded border border-#454443 bg-#090807 p-2 ">
      {children}
    </div>
  )
}

export interface AutomatronHistory {
  historyData: any
}
export function AutomatronHistory(props: AutomatronHistory) {
  return (
    <>
      <div className="flex flex-col">
        {props.historyData.result.history.map(
          (item: { text: string }, index: number) => (
            <div
              key={index}
              className="border-t border-#454443 bg-#090807 p-2 first:border-t-0"
            >
              <pre className="whitespace-pre-wrap font-mono text-sm">
                {item.text}
              </pre>
            </div>
          )
        )}
      </div>
    </>
  )
}

export interface Panel {
  title: ReactNode
  children: ReactNode
}

export function Panel(props: Panel) {
  return (
    <section className="overflow-hidden rounded border border-#454443 bg-#353433 shadow-md shadow-black/50">
      <h2 className="bg-glossy py-1 px-2 font-bold text-#8b8685">
        <span>{props.title}</span>
      </h2>
      {props.children}
    </section>
  )
}

export interface SpeedDial {
  speedDialData: any
}
export function SpeedDial(props: SpeedDial) {
  return (
    <>
      {props.speedDialData.result.speedDials.map((item: { _id: string }) => (
        <Form method="POST" key={item._id} className="flex m-0">
          <input type="hidden" name="text" value={`;SD '${item._id}'`} />
          <button className={clsx(classes.button, 'py-0')} type="submit">
            {item._id}
          </button>
        </Form>
      ))}
    </>
  )
}


================================================
FILE: webui/app/routes/automatron.knobs.tsx
================================================
import { Icon } from '@iconify-icon/react'
import copy from '@iconify-icons/cil/copy'
import { useLoaderData } from '@remix-run/react'
import { backend } from '~/backend'
import { requireAuth } from '~/requireAuth'

export interface AutomatronKnobs {}

export const clientLoader = async () => {
  await requireAuth()
  return {
    knobs: await backend.getKnobs(),
  }
}

export default function AutomatronKnobs() {
  const { knobs } = useLoaderData<typeof clientLoader>()
  return (
    <div className="flex flex-col gap-4">
      {Object.entries(knobs.result.knobs)
        .sort((a, b) => a[0].localeCompare(b[0]))
        .map(([name, value]) => {
          return (
            <div key={name}>
              <div className="flex gap-3">
                <label className="block text-#8b8685">{name}</label>
                <button
                  className="text-#8b8685"
                  onClick={() => {
                    const code = `;;ref('${name}').set(${JSON.stringify(
                      value
                    )})`
                    navigator.clipboard.writeText(code)
                  }}
                >
                  <Icon icon={copy} />
                </button>
              </div>
              <input
                type="text"
                value={String(value)}
                readOnly
                className="bg-emboss hover:border-#555453 block w-full rounded border border-#454443 py-1 px-2 font-mono placeholder-#8b8685 shadow-md shadow-black/50 focus:border-#656463 active:border-#8b8685"
              />
            </div>
          )
        })}
    </div>
  )
}


================================================
FILE: webui/app/routes/automatron.tsx
================================================
import { Link, Outlet, useLocation } from '@remix-run/react'
import { Fragment } from 'react'

export default function AutomatronLayout() {
  const { pathname } = useLocation()
  return (
    <>
      <div className="flex h-16 items-end px-2">
        {Array.from(['automatron', 'knobs']).map((key) => {
          const to = `/automatron${key === 'automatron' ? '' : `/${key}`}`
          return (
            <Fragment key={key}>
              {pathname === to ? (
                <div className="border-x border-t border-#454443 bg-#252423 px-2 py-1 text-sm text-#8b8685">
                  {key}
                </div>
              ) : (
                <Link
                  to={to}
                  className="border-x border-t border-transparent px-2 py-1 text-sm text-#8b8685"
                >
                  {key}
                </Link>
              )}
            </Fragment>
          )
        })}
      </div>
      <div className="-mt-px min-h-screen border-t border-t-#454443 bg-#252423 px-4 py-4">
        <Outlet />
      </div>
    </>
  )
}


================================================
FILE: webui/env.d.ts
================================================
/// <reference types="@remix-run/node" />
/// <reference types="vite/client" />


================================================
FILE: webui/package.json
================================================
{
  "name": "webui-remix",
  "private": true,
  "sideEffects": false,
  "type": "module",
  "scripts": {
    "build": "remix vite:build",
    "dev": "remix vite:dev --port ${PORT:-22301}",
    "lint": "eslint --ignore-path .gitignore --cache --cache-location ./node_modules/.cache/eslint .",
    "start": "remix-serve ./build/server/index.js",
    "typecheck": "tsc"
  },
  "dependencies": {
    "@fontsource/arimo": "^5.0.18",
    "@iconify-icon/react": "^1.0.8",
    "@iconify-icons/cil": "^1.2.4",
    "@nanostores/react": "^0.7.1",
    "@playwright/test": "^1.40.1",
    "@remix-run/node": "^2.12.0",
    "@remix-run/react": "^2.12.0",
    "@remix-run/serve": "^2.12.0",
    "axios": "^1.6.5",
    "clsx": "^1.2.1",
    "firebase": "^10.7.1",
    "isbot": "^4.1.0",
    "nanostores": "^0.9.5",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "sync-external-store": "^1.0.0",
    "zod": "^3.20.2"
  },
  "devDependencies": {
    "@remix-run/dev": "^2.12.0",
    "@types/react": "^18.2.47",
    "@types/react-dom": "^18.2.18",
    "@typescript-eslint/eslint-plugin": "^6.7.4",
    "autoprefixer": "^10.4.13",
    "eslint": "^8.38.0",
    "eslint-config-prettier": "^9.0.0",
    "eslint-import-resolver-typescript": "^3.6.1",
    "eslint-plugin-import": "^2.28.1",
    "eslint-plugin-jsx-a11y": "^6.7.1",
    "eslint-plugin-react": "^7.33.2",
    "eslint-plugin-react-hooks": "^4.6.0",
    "postcss": "^8.4.33",
    "tailwindcss": "^3.4.1",
    "typescript": "^5.9.3",
    "vite": "^5.0.0",
    "vite-tsconfig-paths": "^4.2.1"
  },
  "engines": {
    "node": "24.x"
  }
}


================================================
FILE: webui/playwright-report/index.html
================================================


<!DOCTYPE html>
<html>
  <head>
    <meta charset='UTF-8'>
    <meta name='color-scheme' content='dark light'>
    <meta name='viewport' content='width=device-width, initial-scale=1.0'>
    <title>Playwright Test Report</title>
    <script type="module">var J0=Object.defineProperty;var _0=(e,t,n)=>t in e?J0(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var Rt=(e,t,n)=>(_0(e,typeof t!="symbol"?t+"":t,n),n);(function(){const t=document.createElement("link").relList;if(t&&t.supports&&t.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const s of o)if(s.type==="childList")for(const i of s.addedNodes)i.tagName==="LINK"&&i.rel==="modulepreload"&&r(i)}).observe(document,{childList:!0,subtree:!0});function n(o){const s={};return o.integrity&&(s.integrity=o.integrity),o.referrerPolicy&&(s.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?s.credentials="include":o.crossOrigin==="anonymous"?s.credentials="omit":s.credentials="same-origin",s}function r(o){if(o.ep)return;o.ep=!0;const s=n(o);fetch(o.href,s)}})();var Xn=typeof globalThis<"u"?globalThis:typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:{};function $0(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var tf={exports:{}},xs={},nf={exports:{}},Q={};/**
 * @license React
 * react.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */var Jr=Symbol.for("react.element"),eh=Symbol.for("react.portal"),th=Symbol.for("react.fragment"),nh=Symbol.for("react.strict_mode"),rh=Symbol.for("react.profiler"),oh=Symbol.for("react.provider"),sh=Symbol.for("react.context"),ih=Symbol.for("react.forward_ref"),lh=Symbol.for("react.suspense"),ch=Symbol.for("react.memo"),ah=Symbol.for("react.lazy"),Vc=Symbol.iterator;function uh(e){return e===null||typeof e!="object"?null:(e=Vc&&e[Vc]||e["@@iterator"],typeof e=="function"?e:null)}var rf={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},of=Object.assign,sf={};function tr(e,t,n){this.props=e,this.context=t,this.refs=sf,this.updater=n||rf}tr.prototype.isReactComponent={};tr.prototype.setState=function(e,t){if(typeof e!="object"&&typeof e!="function"&&e!=null)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")};tr.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")};function lf(){}lf.prototype=tr.prototype;function Il(e,t,n){this.props=e,this.context=t,this.refs=sf,this.updater=n||rf}var bl=Il.prototype=new lf;bl.constructor=Il;of(bl,tr.prototype);bl.isPureReactComponent=!0;var Wc=Array.isArray,cf=Object.prototype.hasOwnProperty,Nl={current:null},af={key:!0,ref:!0,__self:!0,__source:!0};function uf(e,t,n){var r,o={},s=null,i=null;if(t!=null)for(r in t.ref!==void 0&&(i=t.ref),t.key!==void 0&&(s=""+t.key),t)cf.call(t,r)&&!af.hasOwnProperty(r)&&(o[r]=t[r]);var l=arguments.length-2;if(l===1)o.children=n;else if(1<l){for(var c=Array(l),a=0;a<l;a++)c[a]=arguments[a+2];o.children=c}if(e&&e.defaultProps)for(r in l=e.defaultProps,l)o[r]===void 0&&(o[r]=l[r]);return{$$typeof:Jr,type:e,key:s,ref:i,props:o,_owner:Nl.current}}function fh(e,t){return{$$typeof:Jr,type:e.type,key:t,ref:e.ref,props:e.props,_owner:e._owner}}function Ol(e){return typeof e=="object"&&e!==null&&e.$$typeof===Jr}function dh(e){var t={"=":"=0",":":"=2"};return"$"+e.replace(/[=:]/g,function(n){return t[n]})}var Gc=/\/+/g;function Vs(e,t){return typeof e=="object"&&e!==null&&e.key!=null?dh(""+e.key):t.toString(36)}function bo(e,t,n,r,o){var s=typeof e;(s==="undefined"||s==="boolean")&&(e=null);var i=!1;if(e===null)i=!0;else switch(s){case"string":case"number":i=!0;break;case"object":switch(e.$$typeof){case Jr:case eh:i=!0}}if(i)return i=e,o=o(i),e=r===""?"."+Vs(i,0):r,Wc(o)?(n="",e!=null&&(n=e.replace(Gc,"$&/")+"/"),bo(o,t,n,"",function(a){return a})):o!=null&&(Ol(o)&&(o=fh(o,n+(!o.key||i&&i.key===o.key?"":(""+o.key).replace(Gc,"$&/")+"/")+e)),t.push(o)),1;if(i=0,r=r===""?".":r+":",Wc(e))for(var l=0;l<e.length;l++){s=e[l];var c=r+Vs(s,l);i+=bo(s,t,n,c,o)}else if(c=uh(e),typeof c=="function")for(e=c.call(e),l=0;!(s=e.next()).done;)s=s.value,c=r+Vs(s,l++),i+=bo(s,t,n,c,o);else if(s==="object")throw t=String(e),Error("Objects are not valid as a React child (found: "+(t==="[object Object]"?"object with keys {"+Object.keys(e).join(", ")+"}":t)+"). If you meant to render a collection of children, use an array instead.");return i}function oo(e,t,n){if(e==null)return e;var r=[],o=0;return bo(e,r,"","",function(s){return t.call(n,s,o++)}),r}function ph(e){if(e._status===-1){var t=e._result;t=t(),t.then(function(n){(e._status===0||e._status===-1)&&(e._status=1,e._result=n)},function(n){(e._status===0||e._status===-1)&&(e._status=2,e._result=n)}),e._status===-1&&(e._status=0,e._result=t)}if(e._status===1)return e._result.default;throw e._result}var ke={current:null},No={transition:null},hh={ReactCurrentDispatcher:ke,ReactCurrentBatchConfig:No,ReactCurrentOwner:Nl};Q.Children={map:oo,forEach:function(e,t,n){oo(e,function(){t.apply(this,arguments)},n)},count:function(e){var t=0;return oo(e,function(){t++}),t},toArray:function(e){return oo(e,function(t){return t})||[]},only:function(e){if(!Ol(e))throw Error("React.Children.only expected to receive a single React element child.");return e}};Q.Component=tr;Q.Fragment=th;Q.Profiler=rh;Q.PureComponent=Il;Q.StrictMode=nh;Q.Suspense=lh;Q.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=hh;Q.cloneElement=function(e,t,n){if(e==null)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+e+".");var r=of({},e.props),o=e.key,s=e.ref,i=e._owner;if(t!=null){if(t.ref!==void 0&&(s=t.ref,i=Nl.current),t.key!==void 0&&(o=""+t.key),e.type&&e.type.defaultProps)var l=e.type.defaultProps;for(c in t)cf.call(t,c)&&!af.hasOwnProperty(c)&&(r[c]=t[c]===void 0&&l!==void 0?l[c]:t[c])}var c=arguments.length-2;if(c===1)r.children=n;else if(1<c){l=Array(c);for(var a=0;a<c;a++)l[a]=arguments[a+2];r.children=l}return{$$typeof:Jr,type:e.type,key:o,ref:s,props:r,_owner:i}};Q.createContext=function(e){return e={$$typeof:sh,_currentValue:e,_currentValue2:e,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null},e.Provider={$$typeof:oh,_context:e},e.Consumer=e};Q.createElement=uf;Q.createFactory=function(e){var t=uf.bind(null,e);return t.type=e,t};Q.createRef=function(){return{current:null}};Q.forwardRef=function(e){return{$$typeof:ih,render:e}};Q.isValidElement=Ol;Q.lazy=function(e){return{$$typeof:ah,_payload:{_status:-1,_result:e},_init:ph}};Q.memo=function(e,t){return{$$typeof:ch,type:e,compare:t===void 0?null:t}};Q.startTransition=function(e){var t=No.transition;No.transition={};try{e()}finally{No.transition=t}};Q.unstable_act=function(){throw Error("act(...) is not supported in production builds of React.")};Q.useCallback=function(e,t){return ke.current.useCallback(e,t)};Q.useContext=function(e){return ke.current.useContext(e)};Q.useDebugValue=function(){};Q.useDeferredValue=function(e){return ke.current.useDeferredValue(e)};Q.useEffect=function(e,t){return ke.current.useEffect(e,t)};Q.useId=function(){return ke.current.useId()};Q.useImperativeHandle=function(e,t,n){return ke.current.useImperativeHandle(e,t,n)};Q.useInsertionEffect=function(e,t){return ke.current.useInsertionEffect(e,t)};Q.useLayoutEffect=function(e,t){return ke.current.useLayoutEffect(e,t)};Q.useMemo=function(e,t){return ke.current.useMemo(e,t)};Q.useReducer=function(e,t,n){return ke.current.useReducer(e,t,n)};Q.useRef=function(e){return ke.current.useRef(e)};Q.useState=function(e){return ke.current.useState(e)};Q.useSyncExternalStore=function(e,t,n){return ke.current.useSyncExternalStore(e,t,n)};Q.useTransition=function(){return ke.current.useTransition()};Q.version="18.1.0";nf.exports=Q;var j=nf.exports;/**
 * @license React
 * react-jsx-runtime.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */var gh=j,mh=Symbol.for("react.element"),vh=Symbol.for("react.fragment"),wh=Object.prototype.hasOwnProperty,yh=gh.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,Ah={key:!0,ref:!0,__self:!0,__source:!0};function ff(e,t,n){var r,o={},s=null,i=null;n!==void 0&&(s=""+n),t.key!==void 0&&(s=""+t.key),t.ref!==void 0&&(i=t.ref);for(r in t)wh.call(t,r)&&!Ah.hasOwnProperty(r)&&(o[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps,t)o[r]===void 0&&(o[r]=t[r]);return{$$typeof:mh,type:e,key:s,ref:i,props:o,_owner:yh.current}}xs.Fragment=vh;xs.jsx=ff;xs.jsxs=ff;tf.exports=xs;var Pl=tf.exports;const yn=Pl.Fragment,A=Pl.jsx,L=Pl.jsxs,Eh=15,V=0,yt=1,xh=2,De=-2,J=-3,Yc=-4,At=-5,Pe=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535],df=1440,Sh=0,kh=4,Ch=9,Dh=5,Rh=[96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,192,80,7,10,0,8,96,0,8,32,0,9,160,0,8,0,0,8,128,0,8,64,0,9,224,80,7,6,0,8,88,0,8,24,0,9,144,83,7,59,0,8,120,0,8,56,0,9,208,81,7,17,0,8,104,0,8,40,0,9,176,0,8,8,0,8,136,0,8,72,0,9,240,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,200,81,7,13,0,8,100,0,8,36,0,9,168,0,8,4,0,8,132,0,8,68,0,9,232,80,7,8,0,8,92,0,8,28,0,9,152,84,7,83,0,8,124,0,8,60,0,9,216,82,7,23,0,8,108,0,8,44,0,9,184,0,8,12,0,8,140,0,8,76,0,9,248,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,196,81,7,11,0,8,98,0,8,34,0,9,164,0,8,2,0,8,130,0,8,66,0,9,228,80,7,7,0,8,90,0,8,26,0,9,148,84,7,67,0,8,122,0,8,58,0,9,212,82,7,19,0,8,106,0,8,42,0,9,180,0,8,10,0,8,138,0,8,74,0,9,244,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,204,81,7,15,0,8,102,0,8,38,0,9,172,0,8,6,0,8,134,0,8,70,0,9,236,80,7,9,0,8,94,0,8,30,0,9,156,84,7,99,0,8,126,0,8,62,0,9,220,82,7,27,0,8,110,0,8,46,0,9,188,0,8,14,0,8,142,0,8,78,0,9,252,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,194,80,7,10,0,8,97,0,8,33,0,9,162,0,8,1,0,8,129,0,8,65,0,9,226,80,7,6,0,8,89,0,8,25,0,9,146,83,7,59,0,8,121,0,8,57,0,9,210,81,7,17,0,8,105,0,8,41,0,9,178,0,8,9,0,8,137,0,8,73,0,9,242,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,202,81,7,13,0,8,101,0,8,37,0,9,170,0,8,5,0,8,133,0,8,69,0,9,234,80,7,8,0,8,93,0,8,29,0,9,154,84,7,83,0,8,125,0,8,61,0,9,218,82,7,23,0,8,109,0,8,45,0,9,186,0,8,13,0,8,141,0,8,77,0,9,250,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,198,81,7,11,0,8,99,0,8,35,0,9,166,0,8,3,0,8,131,0,8,67,0,9,230,80,7,7,0,8,91,0,8,27,0,9,150,84,7,67,0,8,123,0,8,59,0,9,214,82,7,19,0,8,107,0,8,43,0,9,182,0,8,11,0,8,139,0,8,75,0,9,246,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,206,81,7,15,0,8,103,0,8,39,0,9,174,0,8,7,0,8,135,0,8,71,0,9,238,80,7,9,0,8,95,0,8,31,0,9,158,84,7,99,0,8,127,0,8,63,0,9,222,82,7,27,0,8,111,0,8,47,0,9,190,0,8,15,0,8,143,0,8,79,0,9,254,96,7,256,0,8,80,0,8,16,84,8,115,82,7,31,0,8,112,0,8,48,0,9,193,80,7,10,0,8,96,0,8,32,0,9,161,0,8,0,0,8,128,0,8,64,0,9,225,80,7,6,0,8,88,0,8,24,0,9,145,83,7,59,0,8,120,0,8,56,0,9,209,81,7,17,0,8,104,0,8,40,0,9,177,0,8,8,0,8,136,0,8,72,0,9,241,80,7,4,0,8,84,0,8,20,85,8,227,83,7,43,0,8,116,0,8,52,0,9,201,81,7,13,0,8,100,0,8,36,0,9,169,0,8,4,0,8,132,0,8,68,0,9,233,80,7,8,0,8,92,0,8,28,0,9,153,84,7,83,0,8,124,0,8,60,0,9,217,82,7,23,0,8,108,0,8,44,0,9,185,0,8,12,0,8,140,0,8,76,0,9,249,80,7,3,0,8,82,0,8,18,85,8,163,83,7,35,0,8,114,0,8,50,0,9,197,81,7,11,0,8,98,0,8,34,0,9,165,0,8,2,0,8,130,0,8,66,0,9,229,80,7,7,0,8,90,0,8,26,0,9,149,84,7,67,0,8,122,0,8,58,0,9,213,82,7,19,0,8,106,0,8,42,0,9,181,0,8,10,0,8,138,0,8,74,0,9,245,80,7,5,0,8,86,0,8,22,192,8,0,83,7,51,0,8,118,0,8,54,0,9,205,81,7,15,0,8,102,0,8,38,0,9,173,0,8,6,0,8,134,0,8,70,0,9,237,80,7,9,0,8,94,0,8,30,0,9,157,84,7,99,0,8,126,0,8,62,0,9,221,82,7,27,0,8,110,0,8,46,0,9,189,0,8,14,0,8,142,0,8,78,0,9,253,96,7,256,0,8,81,0,8,17,85,8,131,82,7,31,0,8,113,0,8,49,0,9,195,80,7,10,0,8,97,0,8,33,0,9,163,0,8,1,0,8,129,0,8,65,0,9,227,80,7,6,0,8,89,0,8,25,0,9,147,83,7,59,0,8,121,0,8,57,0,9,211,81,7,17,0,8,105,0,8,41,0,9,179,0,8,9,0,8,137,0,8,73,0,9,243,80,7,4,0,8,85,0,8,21,80,8,258,83,7,43,0,8,117,0,8,53,0,9,203,81,7,13,0,8,101,0,8,37,0,9,171,0,8,5,0,8,133,0,8,69,0,9,235,80,7,8,0,8,93,0,8,29,0,9,155,84,7,83,0,8,125,0,8,61,0,9,219,82,7,23,0,8,109,0,8,45,0,9,187,0,8,13,0,8,141,0,8,77,0,9,251,80,7,3,0,8,83,0,8,19,85,8,195,83,7,35,0,8,115,0,8,51,0,9,199,81,7,11,0,8,99,0,8,35,0,9,167,0,8,3,0,8,131,0,8,67,0,9,231,80,7,7,0,8,91,0,8,27,0,9,151,84,7,67,0,8,123,0,8,59,0,9,215,82,7,19,0,8,107,0,8,43,0,9,183,0,8,11,0,8,139,0,8,75,0,9,247,80,7,5,0,8,87,0,8,23,192,8,0,83,7,51,0,8,119,0,8,55,0,9,207,81,7,15,0,8,103,0,8,39,0,9,175,0,8,7,0,8,135,0,8,71,0,9,239,80,7,9,0,8,95,0,8,31,0,9,159,84,7,99,0,8,127,0,8,63,0,9,223,82,7,27,0,8,111,0,8,47,0,9,191,0,8,15,0,8,143,0,8,79,0,9,255],Th=[80,5,1,87,5,257,83,5,17,91,5,4097,81,5,5,89,5,1025,85,5,65,93,5,16385,80,5,3,88,5,513,84,5,33,92,5,8193,82,5,9,90,5,2049,86,5,129,192,5,24577,80,5,2,87,5,385,83,5,25,91,5,6145,81,5,7,89,5,1537,85,5,97,93,5,24577,80,5,4,88,5,769,84,5,49,92,5,12289,82,5,13,90,5,3073,86,5,193,192,5,24577],Ih=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],bh=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,112,112],Nh=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],Oh=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],Tt=15;function ki(){const e=this;let t,n,r,o,s,i;function l(a,p,m,h,x,E,v,g,u,f,d){let y,k,w,S,C,R,D,T,M,O,q,U,B,Y,I;O=0,C=m;do r[a[p+O]]++,O++,C--;while(C!==0);if(r[0]==m)return v[0]=-1,g[0]=0,V;for(T=g[0],R=1;R<=Tt&&r[R]===0;R++);for(D=R,T<R&&(T=R),C=Tt;C!==0&&r[C]===0;C--);for(w=C,T>C&&(T=C),g[0]=T,Y=1<<R;R<C;R++,Y<<=1)if((Y-=r[R])<0)return J;if((Y-=r[C])<0)return J;for(r[C]+=Y,i[1]=R=0,O=1,B=2;--C!==0;)i[B]=R+=r[O],B++,O++;C=0,O=0;do(R=a[p+O])!==0&&(d[i[R]++]=C),O++;while(++C<m);for(m=i[w],i[0]=C=0,O=0,S=-1,U=-T,s[0]=0,q=0,I=0;D<=w;D++)for(y=r[D];y--!==0;){for(;D>U+T;){if(S++,U+=T,I=w-U,I=I>T?T:I,(k=1<<(R=D-U))>y+1&&(k-=y+1,B=D,R<I))for(;++R<I&&!((k<<=1)<=r[++B]);)k-=r[B];if(I=1<<R,f[0]+I>df)return J;s[S]=q=f[0],f[0]+=I,S!==0?(i[S]=C,o[0]=R,o[1]=T,R=C>>>U-T,o[2]=q-s[S-1]-R,u.set(o,(s[S-1]+R)*3)):v[0]=q}for(o[1]=D-U,O>=m?o[0]=128+64:d[O]<h?(o[0]=d[O]<256?0:32+64,o[2]=d[O++]):(o[0]=E[d[O]-h]+16+64,o[2]=x[d[O++]-h]),k=1<<D-U,R=C>>>U;R<I;R+=k)u.set(o,(q+R)*3);for(R=1<<D-1;C&R;R>>>=1)C^=R;for(C^=R,M=(1<<U)-1;(C&M)!=i[S];)S--,U-=T,M=(1<<U)-1}return Y!==0&&w!=1?At:V}function c(a){let p;for(t||(t=[],n=[],r=new Int32Array(Tt+1),o=[],s=new Int32Array(Tt),i=new Int32Array(Tt+1)),n.length<a&&(n=[]),p=0;p<a;p++)n[p]=0;for(p=0;p<Tt+1;p++)r[p]=0;for(p=0;p<3;p++)o[p]=0;s.set(r.subarray(0,Tt),0),i.set(r.subarray(0,Tt+1),0)}e.inflate_trees_bits=function(a,p,m,h,x){let E;return c(19),t[0]=0,E=l(a,0,19,19,null,null,m,p,h,t,n),E==J?x.msg="oversubscribed dynamic bit lengths tree":(E==At||p[0]===0)&&(x.msg="incomplete dynamic bit lengths tree",E=J),E},e.inflate_trees_dynamic=function(a,p,m,h,x,E,v,g,u){let f;return c(288),t[0]=0,f=l(m,0,a,257,Ih,bh,E,h,g,t,n),f!=V||h[0]===0?(f==J?u.msg="oversubscribed literal/length tree":f!=Yc&&(u.msg="incomplete literal/length tree",f=J),f):(c(288),f=l(m,a,p,0,Nh,Oh,v,x,g,t,n),f!=V||x[0]===0&&a>257?(f==J?u.msg="oversubscribed distance tree":f==At?(u.msg="incomplete distance tree",f=J):f!=Yc&&(u.msg="empty distance tree with lengths",f=J),f):V)}}ki.inflate_trees_fixed=function(e,t,n,r){return e[0]=Ch,t[0]=Dh,n[0]=Rh,r[0]=Th,V};const so=0,zc=1,Xc=2,Kc=3,Zc=4,Jc=5,_c=6,Ws=7,$c=8,io=9;function Ph(){const e=this;let t,n=0,r,o=0,s=0,i=0,l=0,c=0,a=0,p=0,m,h=0,x,E=0;function v(g,u,f,d,y,k,w,S){let C,R,D,T,M,O,q,U,B,Y,I,H,N,z,F,G;q=S.next_in_index,U=S.avail_in,M=w.bitb,O=w.bitk,B=w.write,Y=B<w.read?w.read-B-1:w.end-B,I=Pe[g],H=Pe[u];do{for(;O<20;)U--,M|=(S.read_byte(q++)&255)<<O,O+=8;if(C=M&I,R=f,D=d,G=(D+C)*3,(T=R[G])===0){M>>=R[G+1],O-=R[G+1],w.win[B++]=R[G+2],Y--;continue}do{if(M>>=R[G+1],O-=R[G+1],T&16){for(T&=15,N=R[G+2]+(M&Pe[T]),M>>=T,O-=T;O<15;)U--,M|=(S.read_byte(q++)&255)<<O,O+=8;C=M&H,R=y,D=k,G=(D+C)*3,T=R[G];do if(M>>=R[G+1],O-=R[G+1],T&16){for(T&=15;O<T;)U--,M|=(S.read_byte(q++)&255)<<O,O+=8;if(z=R[G+2]+(M&Pe[T]),M>>=T,O-=T,Y-=N,B>=z)F=B-z,B-F>0&&2>B-F?(w.win[B++]=w.win[F++],w.win[B++]=w.win[F++],N-=2):(w.win.set(w.win.subarray(F,F+2),B),B+=2,F+=2,N-=2);else{F=B-z;do F+=w.end;while(F<0);if(T=w.end-F,N>T){if(N-=T,B-F>0&&T>B-F)do w.win[B++]=w.win[F++];while(--T!==0);else w.win.set(w.win.subarray(F,F+T),B),B+=T,F+=T,T=0;F=0}}if(B-F>0&&N>B-F)do w.win[B++]=w.win[F++];while(--N!==0);else w.win.set(w.win.subarray(F,F+N),B),B+=N,F+=N,N=0;break}else if(!(T&64))C+=R[G+2],C+=M&Pe[T],G=(D+C)*3,T=R[G];else return S.msg="invalid distance code",N=S.avail_in-U,N=O>>3<N?O>>3:N,U+=N,q-=N,O-=N<<3,w.bitb=M,w.bitk=O,S.avail_in=U,S.total_in+=q-S.next_in_index,S.next_in_index=q,w.write=B,J;while(!0);break}if(T&64)return T&32?(N=S.avail_in-U,N=O>>3<N?O>>3:N,U+=N,q-=N,O-=N<<3,w.bitb=M,w.bitk=O,S.avail_in=U,S.total_in+=q-S.next_in_index,S.next_in_index=q,w.write=B,yt):(S.msg="invalid literal/length code",N=S.avail_in-U,N=O>>3<N?O>>3:N,U+=N,q-=N,O-=N<<3,w.bitb=M,w.bitk=O,S.avail_in=U,S.total_in+=q-S.next_in_index,S.next_in_index=q,w.write=B,J);if(C+=R[G+2],C+=M&Pe[T],G=(D+C)*3,(T=R[G])===0){M>>=R[G+1],O-=R[G+1],w.win[B++]=R[G+2],Y--;break}}while(!0)}while(Y>=258&&U>=10);return N=S.avail_in-U,N=O>>3<N?O>>3:N,U+=N,q-=N,O-=N<<3,w.bitb=M,w.bitk=O,S.avail_in=U,S.total_in+=q-S.next_in_index,S.next_in_index=q,w.write=B,V}e.init=function(g,u,f,d,y,k){t=so,a=g,p=u,m=f,h=d,x=y,E=k,r=null},e.proc=function(g,u,f){let d,y,k,w=0,S=0,C=0,R,D,T,M;for(C=u.next_in_index,R=u.avail_in,w=g.bitb,S=g.bitk,D=g.write,T=D<g.read?g.read-D-1:g.end-D;;)switch(t){case so:if(T>=258&&R>=10&&(g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,f=v(a,p,m,h,x,E,g,u),C=u.next_in_index,R=u.avail_in,w=g.bitb,S=g.bitk,D=g.write,T=D<g.read?g.read-D-1:g.end-D,f!=V)){t=f==yt?Ws:io;break}s=a,r=m,o=h,t=zc;case zc:for(d=s;S<d;){if(R!==0)f=V;else return g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);R--,w|=(u.read_byte(C++)&255)<<S,S+=8}if(y=(o+(w&Pe[d]))*3,w>>>=r[y+1],S-=r[y+1],k=r[y],k===0){i=r[y+2],t=_c;break}if(k&16){l=k&15,n=r[y+2],t=Xc;break}if(!(k&64)){s=k,o=y/3+r[y+2];break}if(k&32){t=Ws;break}return t=io,u.msg="invalid literal/length code",f=J,g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);case Xc:for(d=l;S<d;){if(R!==0)f=V;else return g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);R--,w|=(u.read_byte(C++)&255)<<S,S+=8}n+=w&Pe[d],w>>=d,S-=d,s=p,r=x,o=E,t=Kc;case Kc:for(d=s;S<d;){if(R!==0)f=V;else return g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);R--,w|=(u.read_byte(C++)&255)<<S,S+=8}if(y=(o+(w&Pe[d]))*3,w>>=r[y+1],S-=r[y+1],k=r[y],k&16){l=k&15,c=r[y+2],t=Zc;break}if(!(k&64)){s=k,o=y/3+r[y+2];break}return t=io,u.msg="invalid distance code",f=J,g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);case Zc:for(d=l;S<d;){if(R!==0)f=V;else return g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);R--,w|=(u.read_byte(C++)&255)<<S,S+=8}c+=w&Pe[d],w>>=d,S-=d,t=Jc;case Jc:for(M=D-c;M<0;)M+=g.end;for(;n!==0;){if(T===0&&(D==g.end&&g.read!==0&&(D=0,T=D<g.read?g.read-D-1:g.end-D),T===0&&(g.write=D,f=g.inflate_flush(u,f),D=g.write,T=D<g.read?g.read-D-1:g.end-D,D==g.end&&g.read!==0&&(D=0,T=D<g.read?g.read-D-1:g.end-D),T===0)))return g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);g.win[D++]=g.win[M++],T--,M==g.end&&(M=0),n--}t=so;break;case _c:if(T===0&&(D==g.end&&g.read!==0&&(D=0,T=D<g.read?g.read-D-1:g.end-D),T===0&&(g.write=D,f=g.inflate_flush(u,f),D=g.write,T=D<g.read?g.read-D-1:g.end-D,D==g.end&&g.read!==0&&(D=0,T=D<g.read?g.read-D-1:g.end-D),T===0)))return g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);f=V,g.win[D++]=i,T--,t=so;break;case Ws:if(S>7&&(S-=8,R++,C--),g.write=D,f=g.inflate_flush(u,f),D=g.write,T=D<g.read?g.read-D-1:g.end-D,g.read!=g.write)return g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);t=$c;case $c:return f=yt,g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);case io:return f=J,g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f);default:return f=De,g.bitb=w,g.bitk=S,u.avail_in=R,u.total_in+=C-u.next_in_index,u.next_in_index=C,g.write=D,g.inflate_flush(u,f)}},e.free=function(){}}const ea=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],kn=0,Gs=1,ta=2,na=3,ra=4,oa=5,lo=6,co=7,sa=8,nn=9;function Lh(e,t){const n=this;let r=kn,o=0,s=0,i=0,l;const c=[0],a=[0],p=new Ph;let m=0,h=new Int32Array(df*3);const x=0,E=new ki;n.bitk=0,n.bitb=0,n.win=new Uint8Array(t),n.end=t,n.read=0,n.write=0,n.reset=function(v,g){g&&(g[0]=x),r==lo&&p.free(v),r=kn,n.bitk=0,n.bitb=0,n.read=n.write=0},n.reset(e,null),n.inflate_flush=function(v,g){let u,f,d;return f=v.next_out_index,d=n.read,u=(d<=n.write?n.write:n.end)-d,u>v.avail_out&&(u=v.avail_out),u!==0&&g==At&&(g=V),v.avail_out-=u,v.total_out+=u,v.next_out.set(n.win.subarray(d,d+u),f),f+=u,d+=u,d==n.end&&(d=0,n.write==n.end&&(n.write=0),u=n.write-d,u>v.avail_out&&(u=v.avail_out),u!==0&&g==At&&(g=V),v.avail_out-=u,v.total_out+=u,v.next_out.set(n.win.subarray(d,d+u),f),f+=u,d+=u),v.next_out_index=f,n.read=d,g},n.proc=function(v,g){let u,f,d,y,k,w,S,C;for(y=v.next_in_index,k=v.avail_in,f=n.bitb,d=n.bitk,w=n.write,S=w<n.read?n.read-w-1:n.end-w;;){let R,D,T,M,O,q,U,B;switch(r){case kn:for(;d<3;){if(k!==0)g=V;else return n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);k--,f|=(v.read_byte(y++)&255)<<d,d+=8}switch(u=f&7,m=u&1,u>>>1){case 0:f>>>=3,d-=3,u=d&7,f>>>=u,d-=u,r=Gs;break;case 1:R=[],D=[],T=[[]],M=[[]],ki.inflate_trees_fixed(R,D,T,M),p.init(R[0],D[0],T[0],0,M[0],0),f>>>=3,d-=3,r=lo;break;case 2:f>>>=3,d-=3,r=na;break;case 3:return f>>>=3,d-=3,r=nn,v.msg="invalid block type",g=J,n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g)}break;case Gs:for(;d<32;){if(k!==0)g=V;else return n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);k--,f|=(v.read_byte(y++)&255)<<d,d+=8}if((~f>>>16&65535)!=(f&65535))return r=nn,v.msg="invalid stored block lengths",g=J,n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);o=f&65535,f=d=0,r=o!==0?ta:m!==0?co:kn;break;case ta:if(k===0||S===0&&(w==n.end&&n.read!==0&&(w=0,S=w<n.read?n.read-w-1:n.end-w),S===0&&(n.write=w,g=n.inflate_flush(v,g),w=n.write,S=w<n.read?n.read-w-1:n.end-w,w==n.end&&n.read!==0&&(w=0,S=w<n.read?n.read-w-1:n.end-w),S===0)))return n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);if(g=V,u=o,u>k&&(u=k),u>S&&(u=S),n.win.set(v.read_buf(y,u),w),y+=u,k-=u,w+=u,S-=u,(o-=u)!==0)break;r=m!==0?co:kn;break;case na:for(;d<14;){if(k!==0)g=V;else return n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);k--,f|=(v.read_byte(y++)&255)<<d,d+=8}if(s=u=f&16383,(u&31)>29||(u>>5&31)>29)return r=nn,v.msg="too many length or distance symbols",g=J,n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);if(u=258+(u&31)+(u>>5&31),!l||l.length<u)l=[];else for(C=0;C<u;C++)l[C]=0;f>>>=14,d-=14,i=0,r=ra;case ra:for(;i<4+(s>>>10);){for(;d<3;){if(k!==0)g=V;else return n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);k--,f|=(v.read_byte(y++)&255)<<d,d+=8}l[ea[i++]]=f&7,f>>>=3,d-=3}for(;i<19;)l[ea[i++]]=0;if(c[0]=7,u=E.inflate_trees_bits(l,c,a,h,v),u!=V)return g=u,g==J&&(l=null,r=nn),n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);i=0,r=oa;case oa:for(;u=s,!(i>=258+(u&31)+(u>>5&31));){let Y,I;for(u=c[0];d<u;){if(k!==0)g=V;else return n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);k--,f|=(v.read_byte(y++)&255)<<d,d+=8}if(u=h[(a[0]+(f&Pe[u]))*3+1],I=h[(a[0]+(f&Pe[u]))*3+2],I<16)f>>>=u,d-=u,l[i++]=I;else{for(C=I==18?7:I-14,Y=I==18?11:3;d<u+C;){if(k!==0)g=V;else return n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);k--,f|=(v.read_byte(y++)&255)<<d,d+=8}if(f>>>=u,d-=u,Y+=f&Pe[C],f>>>=C,d-=C,C=i,u=s,C+Y>258+(u&31)+(u>>5&31)||I==16&&C<1)return l=null,r=nn,v.msg="invalid bit length repeat",g=J,n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);I=I==16?l[C-1]:0;do l[C++]=I;while(--Y!==0);i=C}}if(a[0]=-1,O=[],q=[],U=[],B=[],O[0]=9,q[0]=6,u=s,u=E.inflate_trees_dynamic(257+(u&31),1+(u>>5&31),l,O,q,U,B,h,v),u!=V)return u==J&&(l=null,r=nn),g=u,n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);p.init(O[0],q[0],h,U[0],h,B[0]),r=lo;case lo:if(n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,(g=p.proc(n,v,g))!=yt)return n.inflate_flush(v,g);if(g=V,p.free(v),y=v.next_in_index,k=v.avail_in,f=n.bitb,d=n.bitk,w=n.write,S=w<n.read?n.read-w-1:n.end-w,m===0){r=kn;break}r=co;case co:if(n.write=w,g=n.inflate_flush(v,g),w=n.write,S=w<n.read?n.read-w-1:n.end-w,n.read!=n.write)return n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);r=sa;case sa:return g=yt,n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);case nn:return g=J,n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g);default:return g=De,n.bitb=f,n.bitk=d,v.avail_in=k,v.total_in+=y-v.next_in_index,v.next_in_index=y,n.write=w,n.inflate_flush(v,g)}}},n.free=function(v){n.reset(v,null),n.win=null,h=null},n.set_dictionary=function(v,g,u){n.win.set(v.subarray(g,g+u),0),n.read=n.write=u},n.sync_point=function(){return r==Gs?1:0}}const Mh=32,Bh=8,Hh=0,ia=1,la=2,ca=3,aa=4,ua=5,Ys=6,or=7,fa=12,It=13,Fh=[0,0,255,255];function Uh(){const e=this;e.mode=0,e.method=0,e.was=[0],e.need=0,e.marker=0,e.wbits=0;function t(n){return!n||!n.istate?De:(n.total_in=n.total_out=0,n.msg=null,n.istate.mode=or,n.istate.blocks.reset(n,null),V)}e.inflateEnd=function(n){return e.blocks&&e.blocks.free(n),e.blocks=null,V},e.inflateInit=function(n,r){return n.msg=null,e.blocks=null,r<8||r>15?(e.inflateEnd(n),De):(e.wbits=r,n.istate.blocks=new Lh(n,1<<r),t(n),V)},e.inflate=function(n,r){let o,s;if(!n||!n.istate||!n.next_in)return De;const i=n.istate;for(r=r==kh?At:V,o=At;;)switch(i.mode){case Hh:if(n.avail_in===0)return o;if(o=r,n.avail_in--,n.total_in++,((i.method=n.read_byte(n.next_in_index++))&15)!=Bh){i.mode=It,n.msg="unknown compression method",i.marker=5;break}if((i.method>>4)+8>i.wbits){i.mode=It,n.msg="invalid win size",i.marker=5;break}i.mode=ia;case ia:if(n.avail_in===0)return o;if(o=r,n.avail_in--,n.total_in++,s=n.read_byte(n.next_in_index++)&255,((i.method<<8)+s)%31!==0){i.mode=It,n.msg="incorrect header check",i.marker=5;break}if(!(s&Mh)){i.mode=or;break}i.mode=la;case la:if(n.avail_in===0)return o;o=r,n.avail_in--,n.total_in++,i.need=(n.read_byte(n.next_in_index++)&255)<<24&4278190080,i.mode=ca;case ca:if(n.avail_in===0)return o;o=r,n.avail_in--,n.total_in++,i.need+=(n.read_byte(n.next_in_index++)&255)<<16&16711680,i.mode=aa;case aa:if(n.avail_in===0)return o;o=r,n.avail_in--,n.total_in++,i.need+=(n.read_byte(n.next_in_index++)&255)<<8&65280,i.mode=ua;case ua:return n.avail_in===0?o:(o=r,n.avail_in--,n.total_in++,i.need+=n.read_byte(n.next_in_index++)&255,i.mode=Ys,xh);case Ys:return i.mode=It,n.msg="need dictionary",i.marker=0,De;case or:if(o=i.blocks.proc(n,o),o==J){i.mode=It,i.marker=0;break}if(o==V&&(o=r),o!=yt)return o;o=r,i.blocks.reset(n,i.was),i.mode=fa;case fa:return n.avail_in=0,yt;case It:return J;default:return De}},e.inflateSetDictionary=function(n,r,o){let s=0,i=o;if(!n||!n.istate||n.istate.mode!=Ys)return De;const l=n.istate;return i>=1<<l.wbits&&(i=(1<<l.wbits)-1,s=o-i),l.blocks.set_dictionary(r,s,i),l.mode=or,V},e.inflateSync=function(n){let r,o,s,i,l;if(!n||!n.istate)return De;const c=n.istate;if(c.mode!=It&&(c.mode=It,c.marker=0),(r=n.avail_in)===0)return At;for(o=n.next_in_index,s=c.marker;r!==0&&s<4;)n.read_byte(o)==Fh[s]?s++:n.read_byte(o)!==0?s=0:s=4-s,o++,r--;return n.total_in+=o-n.next_in_index,n.next_in_index=o,n.avail_in=r,c.marker=s,s!=4?J:(i=n.total_in,l=n.total_out,t(n),n.total_in=i,n.total_out=l,c.mode=or,V)},e.inflateSyncPoint=function(n){return!n||!n.istate||!n.istate.blocks?De:n.istate.blocks.sync_point()}}function pf(){}pf.prototype={inflateInit(e){const t=this;return t.istate=new Uh,e||(e=Eh),t.istate.inflateInit(t,e)},inflate(e){const t=this;return t.istate?t.istate.inflate(t,e):De},inflateEnd(){const e=this;if(!e.istate)return De;const t=e.istate.inflateEnd(e);return e.istate=null,t},inflateSync(){const e=this;return e.istate?e.istate.inflateSync(e):De},inflateSetDictionary(e,t){const n=this;return n.istate?n.istate.inflateSetDictionary(n,e,t):De},read_byte(e){return this.next_in[e]},read_buf(e,t){return this.next_in.subarray(e,e+t)}};function qh(e){const t=this,n=new pf,r=e&&e.chunkSize?Math.floor(e.chunkSize*2):128*1024,o=Sh,s=new Uint8Array(r);let i=!1;n.inflateInit(),n.next_out=s,t.append=function(l,c){const a=[];let p,m,h=0,x=0,E=0;if(l.length!==0){n.next_in_index=0,n.next_in=l,n.avail_in=l.length;do{if(n.next_out_index=0,n.avail_out=r,n.avail_in===0&&!i&&(n.next_in_index=0,i=!0),p=n.inflate(o),i&&p===At){if(n.avail_in!==0)throw new Error("inflating: bad input")}else if(p!==V&&p!==yt)throw new Error("inflating: "+n.msg);if((i||p===yt)&&n.avail_in===l.length)throw new Error("inflating: bad input");n.next_out_index&&(n.next_out_index===r?a.push(new Uint8Array(s)):a.push(s.subarray(0,n.next_out_index))),E+=n.next_out_index,c&&n.next_in_index>0&&n.next_in_index!=h&&(c(n.next_in_index),h=n.next_in_index)}while(n.avail_in>0||n.avail_out===0);return a.length>1?(m=new Uint8Array(E),a.forEach(function(v){m.set(v,x),x+=v.length})):m=a[0]?new Uint8Array(a[0]):new Uint8Array,m}},t.flush=function(){n.inflateEnd()}}const un=4294967295,Lt=65535,Qh=8,jh=0,Vh=99,Wh=67324752,Gh=134695760,da=33639248,Yh=101010256,pa=101075792,zh=117853008,ao=22,zs=20,Xs=56,Xh=1,Kh=39169,Zh=10,Jh=1,_h=21589,$h=28789,eg=25461,tg=6534,ha=1,ng=6,ga=8,ma=2048,va=16,rg="/",Ge=void 0,Yo="undefined",hf="function";class wa{constructor(t){return class extends TransformStream{constructor(n,r){const o=new t(r);super({transform(s,i){i.enqueue(o.append(s))},flush(s){const i=o.flush();i&&s.enqueue(i)}})}}}}const og=64;let gf=2;try{typeof navigator!=Yo&&navigator.hardwareConcurrency&&(gf=navigator.hardwareConcurrency)}catch{}const sg={chunkSize:512*1024,maxWorkers:gf,terminateWorkerTimeout:5e3,useWebWorkers:!0,useCompressionStream:!0,workerScripts:Ge,CompressionStreamNative:typeof CompressionStream!=Yo&&CompressionStream,DecompressionStreamNative:typeof DecompressionStream!=Yo&&DecompressionStream},Mt=Object.assign({},sg);function mf(){return Mt}function ig(e){return Math.max(e.chunkSize,og)}function vf(e){const{baseURL:t,chunkSize:n,maxWorkers:r,terminateWorkerTimeout:o,useCompressionStream:s,useWebWorkers:i,Deflate:l,Inflate:c,CompressionStream:a,DecompressionStream:p,workerScripts:m}=e;if(bt("baseURL",t),bt("chunkSize",n),bt("maxWorkers",r),bt("terminateWorkerTimeout",o),bt("useCompressionStream",s),bt("useWebWorkers",i),l&&(Mt.CompressionStream=new wa(l)),c&&(Mt.DecompressionStream=new wa(c)),bt("CompressionStream",a),bt("DecompressionStream",p),m!==Ge){const{deflate:h,inflate:x}=m;if((h||x)&&(Mt.workerScripts||(Mt.workerScripts={})),h){if(!Array.isArray(h))throw new Error("workerScripts.deflate must be an array");Mt.workerScripts.deflate=h}if(x){if(!Array.isArray(x))throw new Error("workerScripts.inflate must be an array");Mt.workerScripts.inflate=x}}}function bt(e,t){t!==Ge&&(Mt[e]=t)}function lg(){return"application/octet-stream"}const wf=[];for(let e=0;e<256;e++){let t=e;for(let n=0;n<8;n++)t&1?t=t>>>1^3988292384:t=t>>>1;wf[e]=t}class zo{constructor(t){this.crc=t||-1}append(t){let n=this.crc|0;for(let r=0,o=t.length|0;r<o;r++)n=n>>>8^wf[(n^t[r])&255];this.crc=n}get(){return~this.crc}}class yf extends TransformStream{constructor(){let t;const n=new zo;super({transform(r,o){n.append(r),o.enqueue(r)},flush(){const r=new Uint8Array(4);new DataView(r.buffer).setUint32(0,n.get()),t.value=r}}),t=this}}function cg(e){if(typeof TextEncoder>"u"){e=unescape(encodeURIComponent(e));const t=new Uint8Array(e.length);for(let n=0;n<t.length;n++)t[n]=e.charCodeAt(n);return t}else return new TextEncoder().encode(e)}const xe={concat(e,t){if(e.length===0||t.length===0)return e.concat(t);const n=e[e.length-1],r=xe.getPartial(n);return r===32?e.concat(t):xe._shiftRight(t,r,n|0,e.slice(0,e.length-1))},bitLength(e){const t=e.length;if(t===0)return 0;const n=e[t-1];return(t-1)*32+xe.getPartial(n)},clamp(e,t){if(e.length*32<t)return e;e=e.slice(0,Math.ceil(t/32));const n=e.length;return t=t&31,n>0&&t&&(e[n-1]=xe.partial(t,e[n-1]&2147483648>>t-1,1)),e},partial(e,t,n){return e===32?t:(n?t|0:t<<32-e)+e*1099511627776},getPartial(e){return Math.round(e/1099511627776)||32},_shiftRight(e,t,n,r){for(r===void 0&&(r=[]);t>=32;t-=32)r.push(n),n=0;if(t===0)return r.concat(e);for(let i=0;i<e.length;i++)r.push(n|e[i]>>>t),n=e[i]<<32-t;const o=e.length?e[e.length-1]:0,s=xe.getPartial(o);return r.push(xe.partial(t+s&31,t+s>32?n:r.pop(),1)),r}},Xo={bytes:{fromBits(e){const n=xe.bitLength(e)/8,r=new Uint8Array(n);let o;for(let s=0;s<n;s++)s&3||(o=e[s/4]),r[s]=o>>>24,o<<=8;return r},toBits(e){const t=[];let n,r=0;for(n=0;n<e.length;n++)r=r<<8|e[n],(n&3)===3&&(t.push(r),r=0);return n&3&&t.push(xe.partial(8*(n&3),r)),t}}},Af={};Af.sha1=class{constructor(e){const t=this;t.blockSize=512,t._init=[1732584193,4023233417,2562383102,271733878,3285377520],t._key=[1518500249,1859775393,2400959708,3395469782],e?(t._h=e._h.slice(0),t._buffer=e._buffer.slice(0),t._length=e._length):t.reset()}reset(){const e=this;return e._h=e._init.slice(0),e._buffer=[],e._length=0,e}update(e){const t=this;typeof e=="string"&&(e=Xo.utf8String.toBits(e));const n=t._buffer=xe.concat(t._buffer,e),r=t._length,o=t._length=r+xe.bitLength(e);if(o>9007199254740991)throw new Error("Cannot hash more than 2^53 - 1 bits");const s=new Uint32Array(n);let i=0;for(let l=t.blockSize+r-(t.blockSize+r&t.blockSize-1);l<=o;l+=t.blockSize)t._block(s.subarray(16*i,16*(i+1))),i+=1;return n.splice(0,16*i),t}finalize(){const e=this;let t=e._buffer;const n=e._h;t=xe.concat(t,[xe.partial(1,1)]);for(let r=t.length+2;r&15;r++)t.push(0);for(t.push(Math.floor(e._length/4294967296)),t.push(e._length|0);t.length;)e._block(t.splice(0,16));return e.reset(),n}_f(e,t,n,r){if(e<=19)return t&n|~t&r;if(e<=39)return t^n^r;if(e<=59)return t&n|t&r|n&r;if(e<=79)return t^n^r}_S(e,t){return t<<e|t>>>32-e}_block(e){const t=this,n=t._h,r=Array(80);for(let a=0;a<16;a++)r[a]=e[a];let o=n[0],s=n[1],i=n[2],l=n[3],c=n[4];for(let a=0;a<=79;a++){a>=16&&(r[a]=t._S(1,r[a-3]^r[a-8]^r[a-14]^r[a-16]));const p=t._S(5,o)+t._f(a,s,i,l)+c+r[a]+t._key[Math.floor(a/20)]|0;c=l,l=i,i=t._S(30,s),s=o,o=p}n[0]=n[0]+o|0,n[1]=n[1]+s|0,n[2]=n[2]+i|0,n[3]=n[3]+l|0,n[4]=n[4]+c|0}};const Ef={};Ef.aes=class{constructor(e){const t=this;t._tables=[[[],[],[],[],[]],[[],[],[],[],[]]],t._tables[0][0][0]||t._precompute();const n=t._tables[0][4],r=t._tables[1],o=e.length;let s,i,l,c=1;if(o!==4&&o!==6&&o!==8)throw new Error("invalid aes key size");for(t._key=[i=e.slice(0),l=[]],s=o;s<4*o+28;s++){let a=i[s-1];(s%o===0||o===8&&s%o===4)&&(a=n[a>>>24]<<24^n[a>>16&255]<<16^n[a>>8&255]<<8^n[a&255],s%o===0&&(a=a<<8^a>>>24^c<<24,c=c<<1^(c>>7)*283)),i[s]=i[s-o]^a}for(let a=0;s;a++,s--){const p=i[a&3?s:s-4];s<=4||a<4?l[a]=p:l[a]=r[0][n[p>>>24]]^r[1][n[p>>16&255]]^r[2][n[p>>8&255]]^r[3][n[p&255]]}}encrypt(e){return this._crypt(e,0)}decrypt(e){return this._crypt(e,1)}_precompute(){const e=this._tables[0],t=this._tables[1],n=e[4],r=t[4],o=[],s=[];let i,l,c,a;for(let p=0;p<256;p++)s[(o[p]=p<<1^(p>>7)*283)^p]=p;for(let p=i=0;!n[p];p^=l||1,i=s[i]||1){let m=i^i<<1^i<<2^i<<3^i<<4;m=m>>8^m&255^99,n[p]=m,r[m]=p,a=o[c=o[l=o[p]]];let h=a*16843009^c*65537^l*257^p*16843008,x=o[m]*257^m*16843008;for(let E=0;E<4;E++)e[E][p]=x=x<<24^x>>>8,t[E][m]=h=h<<24^h>>>8}for(let p=0;p<5;p++)e[p]=e[p].slice(0),t[p]=t[p].slice(0)}_crypt(e,t){if(e.length!==4)throw new Error("invalid aes block size");const n=this._key[t],r=n.length/4-2,o=[0,0,0,0],s=this._tables[t],i=s[0],l=s[1],c=s[2],a=s[3],p=s[4];let m=e[0]^n[0],h=e[t?3:1]^n[1],x=e[2]^n[2],E=e[t?1:3]^n[3],v=4,g,u,f;for(let d=0;d<r;d++)g=i[m>>>24]^l[h>>16&255]^c[x>>8&255]^a[E&255]^n[v],u=i[h>>>24]^l[x>>16&255]^c[E>>8&255]^a[m&255]^n[v+1],f=i[x>>>24]^l[E>>16&255]^c[m>>8&255]^a[h&255]^n[v+2],E=i[E>>>24]^l[m>>16&255]^c[h>>8&255]^a[x&255]^n[v+3],v+=4,m=g,h=u,x=f;for(let d=0;d<4;d++)o[t?3&-d:d]=p[m>>>24]<<24^p[h>>16&255]<<16^p[x>>8&255]<<8^p[E&255]^n[v++],g=m,m=h,h=x,x=E,E=g;return o}};const ag={getRandomValues(e){const t=new Uint32Array(e.buffer),n=r=>{let o=987654321;const s=4294967295;return function(){return o=36969*(o&65535)+(o>>16)&s,r=18e3*(r&65535)+(r>>16)&s,(((o<<16)+r&s)/4294967296+.5)*(Math.random()>.5?1:-1)}};for(let r=0,o;r<e.length;r+=4){const s=n((o||Math.random())*4294967296);o=s()*987654071,t[r/4]=s()*4294967296|0}return e}},xf={};xf.ctrGladman=class{constructor(e,t){this._prf=e,this._initIv=t,this._iv=t}reset(){this._iv=this._initIv}update(e){return this.calculate(this._prf,e,this._iv)}incWord(e){if((e>>24&255)===255){let t=e>>16&255,n=e>>8&255,r=e&255;t===255?(t=0,n===255?(n=0,r===255?r=0:++r):++n):++t,e=0,e+=t<<16,e+=n<<8,e+=r}else e+=1<<24;return e}incCounter(e){(e[0]=this.incWord(e[0]))===0&&(e[1]=this.incWord(e[1]))}calculate(e,t,n){let r;if(!(r=t.length))return[];const o=xe.bitLength(t);for(let s=0;s<r;s+=4){this.incCounter(n);const i=e.encrypt(n);t[s]^=i[0],t[s+1]^=i[1],t[s+2]^=i[2],t[s+3]^=i[3]}return xe.clamp(t,o)}};const pn={importKey(e){return new pn.hmacSha1(Xo.bytes.toBits(e))},pbkdf2(e,t,n,r){if(n=n||1e4,r<0||n<0)throw new Error("invalid params to pbkdf2");const o=(r>>5)+1<<2;let s,i,l,c,a;const p=new ArrayBuffer(o),m=new DataView(p);let h=0;const x=xe;for(t=Xo.bytes.toBits(t),a=1;h<(o||1);a++){for(s=i=e.encrypt(x.concat(t,[a])),l=1;l<n;l++)for(i=e.encrypt(i),c=0;c<i.length;c++)s[c]^=i[c];for(l=0;h<(o||1)&&l<s.length;l++)m.setInt32(h,s[l]),h+=4}return p.slice(0,r/8)}};pn.hmacSha1=class{constructor(e){const t=this,n=t._hash=Af.sha1,r=[[],[]];t._baseHash=[new n,new n];const o=t._baseHash[0].blockSize/32;e.length>o&&(e=new n().update(e).finalize());for(let s=0;s<o;s++)r[0][s]=e[s]^909522486,r[1][s]=e[s]^1549556828;t._baseHash[0].update(r[0]),t._baseHash[1].update(r[1]),t._resultHash=new n(t._baseHash[0])}reset(){const e=this;e._resultHash=new e._hash(e._baseHash[0]),e._updated=!1}update(e){const t=this;t._updated=!0,t._resultHash.update(e)}digest(){const e=this,t=e._resultHash.finalize(),n=new e._hash(e._baseHash[1]).update(t).finalize();return e.reset(),n}encrypt(e){if(this._updated)throw new Error("encrypt on already updated hmac called!");return this.update(e),this.digest(e)}};const ug=typeof crypto<"u"&&typeof crypto.getRandomValues=="function",Ll="Invalid password",Ml="Invalid signature",Bl="zipjs-abort-check-password";function Sf(e){return ug?crypto.getRandomValues(e):ag.getRandomValues(e)}const Rn=16,fg="raw",kf={name:"PBKDF2"},dg={name:"HMAC"},pg="SHA-1",hg=Object.assign({hash:dg},kf),Ci=Object.assign({iterations:1e3,hash:{name:pg}},kf),gg=["deriveBits"],Tr=[8,12,16],sr=[16,24,32],Ot=10,mg=[0,0,0,0],Cf="undefined",Df="function",Ss=typeof crypto!=Cf,_r=Ss&&crypto.subtle,Rf=Ss&&typeof _r!=Cf,ut=Xo.bytes,vg=Ef.aes,wg=xf.ctrGladman,yg=pn.hmacSha1;let ya=Ss&&Rf&&typeof _r.importKey==Df,Aa=Ss&&Rf&&typeof _r.deriveBits==Df;class Ag extends TransformStream{constructor({password:t,signed:n,encryptionStrength:r,checkPasswordOnly:o}){super({start(){Object.assign(this,{ready:new Promise(s=>this.resolveReady=s),password:t,signed:n,strength:r-1,pending:new Uint8Array})},async transform(s,i){const l=this,{password:c,strength:a,resolveReady:p,ready:m}=l;c?(await xg(l,a,c,Ve(s,0,Tr[a]+2)),s=Ve(s,Tr[a]+2),o?i.error(new Error(Bl)):p()):await m;const h=new Uint8Array(s.length-Ot-(s.length-Ot)%Rn);i.enqueue(Tf(l,s,h,0,Ot,!0))},async flush(s){const{signed:i,ctr:l,hmac:c,pending:a,ready:p}=this;await p;const m=Ve(a,0,a.length-Ot),h=Ve(a,a.length-Ot);let x=new Uint8Array;if(m.length){const E=br(ut,m);c.update(E);const v=l.update(E);x=Ir(ut,v)}if(i){const E=Ve(Ir(ut,c.digest()),0,Ot);for(let v=0;v<Ot;v++)if(E[v]!=h[v])throw new Error(Ml)}s.enqueue(x)}})}}class Eg extends TransformStream{constructor({password:t,encryptionStrength:n}){let r;super({start(){Object.assign(this,{ready:new Promise(o=>this.resolveReady=o),password:t,strength:n-1,pending:new Uint8Array})},async transform(o,s){const i=this,{password:l,strength:c,resolveReady:a,ready:p}=i;let m=new Uint8Array;l?(m=await Sg(i,c,l),a()):await p;const h=new Uint8Array(m.length+o.length-o.length%Rn);h.set(m,0),s.enqueue(Tf(i,o,h,m.length,0))},async flush(o){const{ctr:s,hmac:i,pending:l,ready:c}=this;await c;let a=new Uint8Array;if(l.length){const p=s.update(br(ut,l));i.update(p),a=Ir(ut,p)}r.signature=Ir(ut,i.digest()).slice(0,Ot),o.enqueue(Hl(a,r.signature))}}),r=this}}function Tf(e,t,n,r,o,s){const{ctr:i,hmac:l,pending:c}=e,a=t.length-o;c.length&&(t=Hl(c,t),n=Dg(n,a-a%Rn));let p;for(p=0;p<=a-Rn;p+=Rn){const m=br(ut,Ve(t,p,p+Rn));s&&l.update(m);const h=i.update(m);s||l.update(h),n.set(Ir(ut,h),p+r)}return e.pending=Ve(t,p),n}async function xg(e,t,n,r){const o=await If(e,t,n,Ve(r,0,Tr[t])),s=Ve(r,Tr[t]);if(o[0]!=s[0]||o[1]!=s[1])throw new Error(Ll)}async function Sg(e,t,n){const r=Sf(new Uint8Array(Tr[t])),o=await If(e,t,n,r);return Hl(r,o)}async function If(e,t,n,r){e.password=null;const o=cg(n),s=await kg(fg,o,hg,!1,gg),i=await Cg(Object.assign({salt:r},Ci),s,8*(sr[t]*2+2)),l=new Uint8Array(i),c=br(ut,Ve(l,0,sr[t])),a=br(ut,Ve(l,sr[t],sr[t]*2)),p=Ve(l,sr[t]*2);return Object.assign(e,{keys:{key:c,authentication:a,passwordVerification:p},ctr:new wg(new vg(c),Array.from(mg)),hmac:new yg(a)}),p}async function kg(e,t,n,r,o){if(ya)try{return await _r.importKey(e,t,n,r,o)}catch{return ya=!1,pn.importKey(t)}else return pn.importKey(t)}async function Cg(e,t,n){if(Aa)try{return await _r.deriveBits(e,t,n)}catch{return Aa=!1,pn.pbkdf2(t,e.salt,Ci.iterations,n)}else return pn.pbkdf2(t,e.salt,Ci.iterations,n)}function Hl(e,t){let n=e;return e.length+t.length&&(n=new Uint8Array(e.length+t.length),n.set(e,0),n.set(t,e.length)),n}function Dg(e,t){if(t&&t>e.length){const n=e;e=new Uint8Array(t),e.set(n,0)}return e}function Ve(e,t,n){return e.subarray(t,n)}function Ir(e,t){return e.fromBits(t)}function br(e,t){return e.toBits(t)}const qn=12;class Rg extends TransformStream{constructor({password:t,passwordVerification:n,checkPasswordOnly:r}){super({start(){Object.assign(this,{password:t,passwordVerification:n}),bf(this,t)},transform(o,s){const i=this;if(i.password){const l=Ea(i,o.subarray(0,qn));if(i.password=null,l[qn-1]!=i.passwordVerification)throw new Error(Ll);o=o.subarray(qn)}r?s.error(new Error(Bl)):s.enqueue(Ea(i,o))}})}}class Tg extends TransformStream{constructor({password:t,passwordVerification:n}){super({start(){Object.assign(this,{password:t,passwordVerification:n}),bf(this,t)},transform(r,o){const s=this;let i,l;if(s.password){s.password=null;const c=Sf(new Uint8Array(qn));c[qn-1]=s.passwordVerification,i=new Uint8Array(r.length+c.length),i.set(xa(s,c),0),l=qn}else i=new Uint8Array(r.length),l=0;i.set(xa(s,r),l),o.enqueue(i)}})}}function Ea(e,t){const n=new Uint8Array(t.length);for(let r=0;r<t.length;r++)n[r]=Nf(e)^t[r],Fl(e,n[r]);return n}function xa(e,t){const n=new Uint8Array(t.length);for(let r=0;r<t.length;r++)n[r]=Nf(e)^t[r],Fl(e,t[r]);return n}function bf(e,t){const n=[305419896,591751049,878082192];Object.assign(e,{keys:n,crcKey0:new zo(n[0]),crcKey2:new zo(n[2])});for(let r=0;r<t.length;r++)Fl(e,t.charCodeAt(r))}function Fl(e,t){let[n,r,o]=e.keys;e.crcKey0.append([t]),n=~e.crcKey0.get(),r=Sa(Math.imul(Sa(r+Of(n)),134775813)+1),e.crcKey2.append([r>>>24]),o=~e.crcKey2.get(),e.keys=[n,r,o]}function Nf(e){const t=e.keys[2]|2;return Of(Math.imul(t,t^1)>>>8)}function Of(e){return e&255}function Sa(e){return e&4294967295}const ka="deflate-raw";class Ig extends TransformStream{constructor(t,{chunkSize:n,CompressionStream:r,CompressionStreamNative:o}){super({});const{compressed:s,encrypted:i,useCompressionStream:l,zipCrypto:c,signed:a,level:p}=t,m=this;let h,x,E=Pf(super.readable);(!i||c)&&a&&(h=new yf,E=ft(E,h)),s&&(E=Mf(E,l,{level:p,chunkSize:n},o,r)),i&&(c?E=ft(E,new Tg(t)):(x=new Eg(t),E=ft(E,x))),Lf(m,E,()=>{let v;i&&!c&&(v=x.signature),(!i||c)&&a&&(v=new DataView(h.value.buffer).getUint32(0)),m.signature=v})}}class bg extends TransformStream{constructor(t,{chunkSize:n,DecompressionStream:r,DecompressionStreamNative:o}){super({});const{zipCrypto:s,encrypted:i,signed:l,signature:c,compressed:a,useCompressionStream:p}=t;let m,h,x=Pf(super.readable);i&&(s?x=ft(x,new Rg(t)):(h=new Ag(t),x=ft(x,h))),a&&(x=Mf(x,p,{chunkSize:n},o,r)),(!i||s)&&l&&(m=new yf,x=ft(x,m)),Lf(this,x,()=>{if((!i||s)&&l){const E=new DataView(m.value.buffer);if(c!=E.getUint32(0,!1))throw new Error(Ml)}})}}function Pf(e){return ft(e,new TransformStream({transform(t,n){t&&t.length&&n.enqueue(t)}}))}function Lf(e,t,n){t=ft(t,new TransformStream({flush:n})),Object.defineProperty(e,"readable",{get(){return t}})}function Mf(e,t,n,r,o){try{const s=t&&r?r:o;e=ft(e,new s(ka,n))}catch(s){if(t)e=ft(e,new o(ka,n));else throw s}return e}function ft(e,t){return e.pipeThrough(t)}const Ng="message",Og="start",Pg="pull",Ca="data",Lg="ack",Mg="close",Bg="deflate",Bf="inflate";class Hg extends TransformStream{constructor(t,n){super({});const r=this,{codecType:o}=t;let s;o.startsWith(Bg)?s=Ig:o.startsWith(Bf)&&(s=bg);let i=0;const l=new s(t,n),c=super.readable,a=new TransformStream({transform(p,m){p&&p.length&&(i+=p.length,m.enqueue(p))},flush(){const{signature:p}=l;Object.assign(r,{signature:p,size:i})}});Object.defineProperty(r,"readable",{get(){return c.pipeThrough(l).pipeThrough(a)}})}}const Fg=typeof Worker!=Yo;class Ks{constructor(t,{readable:n,writable:r},{options:o,config:s,streamOptions:i,useWebWorkers:l,transferStreams:c,scripts:a},p){const{signal:m}=i;return Object.assign(t,{busy:!0,readable:n.pipeThrough(new Ug(n,i,s),{signal:m}),writable:r,options:Object.assign({},o),scripts:a,transferStreams:c,terminate(){const{worker:h,busy:x}=t;h&&!x&&(h.terminate(),t.interface=null)},onTaskFinished(){t.busy=!1,p(t)}}),(l&&Fg?Qg:qg)(t,s)}}class Ug extends TransformStream{constructor(t,{onstart:n,onprogress:r,size:o,onend:s},{chunkSize:i}){let l=0;super({start(){n&&Zs(n,o)},async transform(c,a){l+=c.length,r&&await Zs(r,l,o),a.enqueue(c)},flush(){t.size=l,s&&Zs(s,l)}},{highWaterMark:1,size:()=>i})}}async function Zs(e,...t){try{await e(...t)}catch{}}function qg(e,t){return{run:()=>jg(e,t)}}function Qg(e,{baseURL:t,chunkSize:n}){return e.interface||Object.assign(e,{worker:Gg(e.scripts[0],t,e),interface:{run:()=>Vg(e,{chunkSize:n})}}),e.interface}async function jg({options:e,readable:t,writable:n,onTaskFinished:r},o){const s=new Hg(e,o);try{await t.pipeThrough(s).pipeTo(n,{preventClose:!0,preventAbort:!0});const{signature:i,size:l}=s;return{signature:i,size:l}}finally{r()}}async function Vg(e,t){let n,r;const o=new Promise((h,x)=>{n=h,r=x});Object.assign(e,{reader:null,writer:null,resolveResult:n,rejectResult:r,result:o});const{readable:s,options:i,scripts:l}=e,{writable:c,closed:a}=Wg(e.writable);Di({type:Og,scripts:l.slice(1),options:i,config:t,readable:s,writable:c},e)||Object.assign(e,{reader:s.getReader(),writer:c.getWriter()});const m=await o;try{await c.getWriter().close()}catch{}return await a,m}function Wg(e){const t=e.getWriter();let n;const r=new Promise(s=>n=s);return{writable:new WritableStream({async write(s){await t.ready,await t.write(s)},close(){t.releaseLock(),n()},abort(s){return t.abort(s)}}),closed:r}}let Da=!0,Ra=!0;function Gg(e,t,n){const r={type:"module"};let o,s;typeof e==hf&&(e=e());try{o=new URL(e,t)}catch{o=e}if(Da)try{s=new Worker(o)}catch{Da=!1,s=new Worker(o,r)}else s=new Worker(o,r);return s.addEventListener(Ng,i=>Yg(i,n)),s}function Di(e,{worker:t,writer:n,onTaskFinished:r,transferStreams:o}){try{let{value:s,readable:i,writable:l}=e;const c=[];if(s&&(e.value=s.buffer,c.push(e.value)),o&&Ra?(i&&c.push(i),l&&c.push(l)):e.readable=e.writable=null,c.length)try{return t.postMessage(e,c),!0}catch{Ra=!1,e.readable=e.writable=null,t.postMessage(e)}else t.postMessage(e)}catch(s){throw n&&n.releaseLock(),r(),s}}async function Yg({data:e},t){const{type:n,value:r,messageId:o,result:s,error:i}=e,{reader:l,writer:c,resolveResult:a,rejectResult:p,onTaskFinished:m}=t;try{if(i){const{message:x,stack:E,code:v,name:g}=i,u=new Error(x);Object.assign(u,{stack:E,code:v,name:g}),h(u)}else{if(n==Pg){const{value:x,done:E}=await l.read();Di({type:Ca,value:x,done:E,messageId:o},t)}n==Ca&&(await c.ready,await c.write(new Uint8Array(r)),Di({type:Lg,messageId:o},t)),n==Mg&&h(null,s)}}catch(x){h(x)}function h(x,E){x?p(x):a(E),c&&c.releaseLock(),m()}}let Pt=[];const Js=[];let Ta=0;async function zg(e,t){const{options:n,config:r}=t,{transferStreams:o,useWebWorkers:s,useCompressionStream:i,codecType:l,compressed:c,signed:a,encrypted:p}=n,{workerScripts:m,maxWorkers:h,terminateWorkerTimeout:x}=r;t.transferStreams=o||o===Ge;const E=!c&&!a&&!p&&!t.transferStreams;t.useWebWorkers=!E&&(s||s===Ge&&r.useWebWorkers),t.scripts=t.useWebWorkers&&m?m[l]:[],n.useCompressionStream=i||i===Ge&&r.useCompressionStream;let v;const g=Pt.find(f=>!f.busy);if(g)Ri(g),v=new Ks(g,e,t,u);else if(Pt.length<h){const f={indexWorker:Ta};Ta++,Pt.push(f),v=new Ks(f,e,t,u)}else v=await new Promise(f=>Js.push({resolve:f,stream:e,workerOptions:t}));return v.run();function u(f){if(Js.length){const[{resolve:d,stream:y,workerOptions:k}]=Js.splice(0,1);d(new Ks(f,y,k,u))}else f.worker?(Ri(f),Number.isFinite(x)&&x>=0&&(f.terminateTimeout=setTimeout(()=>{Pt=Pt.filter(d=>d!=f),f.terminate()},x))):Pt=Pt.filter(d=>d!=f)}}function Ri(e){const{terminateTimeout:t}=e;t&&(clearTimeout(t),e.terminateTimeout=null)}function Xg(){Pt.forEach(e=>{Ri(e),e.terminate()})}const Hf="HTTP error ",$r="HTTP Range not supported",Ff="Writer iterator completed too soon",Kg="text/plain",Zg="Content-Length",Jg="Content-Range",_g="Accept-Ranges",$g="Range",em="Content-Type",tm="HEAD",Ul="GET",Uf="bytes",nm=64*1024,ql="writable";class ks{constructor(){this.size=0}init(){this.initialized=!0}}class _t extends ks{get readable(){const t=this,{chunkSize:n=nm}=t,r=new ReadableStream({start(){this.chunkOffset=0},async pull(o){const{offset:s=0,size:i,diskNumberStart:l}=r,{chunkOffset:c}=this;o.enqueue(await he(t,s+c,Math.min(n,i-c),l)),c+n>i?o.close():this.chunkOffset+=n}});return r}}class Ql extends ks{constructor(){super();const t=this,n=new WritableStream({write(r){return t.writeUint8Array(r)}});Object.defineProperty(t,ql,{get(){return n}})}writeUint8Array(){}}class rm extends _t{constructor(t){super();let n=t.length;for(;t.charAt(n-1)=="=";)n--;const r=t.indexOf(",")+1;Object.assign(this,{dataURI:t,dataStart:r,size:Math.floor((n-r)*.75)})}readUint8Array(t,n){const{dataStart:r,dataURI:o}=this,s=new Uint8Array(n),i=Math.floor(t/3)*4,l=atob(o.substring(i+r,Math.ceil((t+n)/3)*4+r)),c=t-Math.floor(i/4)*3;for(let a=c;a<c+n;a++)s[a-c]=l.charCodeAt(a);return s}}class om extends Ql{constructor(t){super(),Object.assign(this,{data:"data:"+(t||"")+";base64,",pending:[]})}writeUint8Array(t){const n=this;let r=0,o=n.pending;const s=n.pending.length;for(n.pending="",r=0;r<Math.floor((s+t.length)/3)*3-s;r++)o+=String.fromCharCode(t[r]);for(;r<t.length;r++)n.pending+=String.fromCharCode(t[r]);o.length>2?n.data+=btoa(o):n.pending=o}getData(){return this.data+btoa(this.pending)}}class jl extends _t{constructor(t){super(),Object.assign(this,{blob:t,size:t.size})}async readUint8Array(t,n){const r=this,o=t+n;let i=await(t||o<r.size?r.blob.slice(t,o):r.blob).arrayBuffer();return i.byteLength>n&&(i=i.slice(t,o)),new Uint8Array(i)}}class qf extends ks{constructor(t){super();const n=this,r=new TransformStream,o=[];t&&o.push([em,t]),Object.defineProperty(n,ql,{get(){return r.writable}}),n.blob=new Response(r.readable,{headers:o}).blob()}getData(){return this.blob}}class sm extends jl{constructor(t){super(new Blob([t],{type:Kg}))}}class im extends qf{constructor(t){super(t),Object.assign(this,{encoding:t,utf8:!t||t.toLowerCase()=="utf-8"})}async getData(){const{encoding:t,utf8:n}=this,r=await super.getData();if(r.text&&n)return r.text();{const o=new FileReader;return new Promise((s,i)=>{Object.assign(o,{onload:({target:l})=>s(l.result),onerror:()=>i(o.error)}),o.readAsText(r,t)})}}}class lm extends _t{constructor(t,n){super(),Qf(this,t,n)}async init(){await jf(this,Ti,Ia),super.init()}readUint8Array(t,n){return Vf(this,t,n,Ti,Ia)}}class cm extends _t{constructor(t,n){super(),Qf(this,t,n)}async init(){await jf(this,Ii,ba),super.init()}readUint8Array(t,n){return Vf(this,t,n,Ii,ba)}}function Qf(e,t,n){const{preventHeadRequest:r,useRangeHeader:o,forceRangeRequests:s}=n;n=Object.assign({},n),delete n.preventHeadRequest,delete n.useRangeHeader,delete n.forceRangeRequests,delete n.useXHR,Object.assign(e,{url:t,options:n,preventHeadRequest:r,useRangeHeader:o,forceRangeRequests:s})}async function jf(e,t,n){const{url:r,useRangeHeader:o,forceRangeRequests:s}=e;if(dm(r)&&(o||s)){const{headers:i}=await t(Ul,e,Wf(e));if(!s&&i.get(_g)!=Uf)throw new Error($r);{let l;const c=i.get(Jg);if(c){const a=c.trim().split(/\s*\/\s*/);if(a.length){const p=a[1];p&&p!="*"&&(l=Number(p))}}l===Ge?await Na(e,t,n):e.size=l}}else await Na(e,t,n)}async function Vf(e,t,n,r,o){const{useRangeHeader:s,forceRangeRequests:i,options:l}=e;if(s||i){const c=await r(Ul,e,Wf(e,t,n));if(c.status!=206)throw new Error($r);return new Uint8Array(await c.arrayBuffer())}else{const{data:c}=e;return c||await o(e,l),new Uint8Array(e.data.subarray(t,t+n))}}function Wf(e,t=0,n=1){return Object.assign({},Vl(e),{[$g]:Uf+"="+t+"-"+(t+n-1)})}function Vl({options:e}){const{headers:t}=e;if(t)return Symbol.iterator in t?Object.fromEntries(t):t}async function Ia(e){await Gf(e,Ti)}async function ba(e){await Gf(e,Ii)}async function Gf(e,t){const n=await t(Ul,e,Vl(e));e.data=new Uint8Array(await n.arrayBuffer()),e.size||(e.size=e.data.length)}async function Na(e,t,n){if(e.preventHeadRequest)await n(e,e.options);else{const o=(await t(tm,e,Vl(e))).headers.get(Zg);o?e.size=Number(o):await n(e,e.options)}}async function Ti(e,{options:t,url:n},r){const o=await fetch(n,Object.assign({},t,{method:e,headers:r}));if(o.status<400)return o;throw o.status==416?new Error($r):new Error(Hf+(o.statusText||o.status))}function Ii(e,{url:t},n){return new Promise((r,o)=>{const s=new XMLHttpRequest;if(s.addEventListener("load",()=>{if(s.status<400){const i=[];s.getAllResponseHeaders().trim().split(/[\r\n]+/).forEach(l=>{const c=l.trim().split(/\s*:\s*/);c[0]=c[0].trim().replace(/^[a-z]|-[a-z]/g,a=>a.toUpperCase()),i.push(c)}),r({status:s.status,arrayBuffer:()=>s.response,headers:new Map(i)})}else o(s.status==416?new Error($r):new Error(Hf+(s.statusText||s.status)))},!1),s.addEventListener("error",i=>o(i.detail?i.detail.error:new Error("Network error")),!1),s.open(e,t),n)for(const i of Object.entries(n))s.setRequestHeader(i[0],i[1]);s.responseType="arraybuffer",s.send()})}class Yf extends _t{constructor(t,n={}){super(),Object.assign(this,{url:t,reader:n.useXHR?new cm(t,n):new lm(t,n)})}set size(t){}get size(){return this.reader.size}async init(){await this.reader.init(),super.init()}readUint8Array(t,n){return this.reader.readUint8Array(t,n)}}class am extends Yf{constructor(t,n={}){n.useRangeHeader=!0,super(t,n)}}class um extends _t{constructor(t){super(),Object.assign(this,{array:t,size:t.length})}readUint8Array(t,n){return this.array.slice(t,t+n)}}class fm extends Ql{init(t=0){Object.assign(this,{offset:0,array:new Uint8Array(t)}),super.init()}writeUint8Array(t){const n=this;if(n.offset+t.length>n.array.length){const r=n.array;n.array=new Uint8Array(r.length+t.length),n.array.set(r)}n.array.set(t,n.offset),n.offset+=t.length}getData(){return this.array}}class Wl extends _t{constructor(t){super(),this.readers=t}async init(){const t=this,{readers:n}=t;t.lastDiskNumber=0,t.lastDiskOffset=0,await Promise.all(n.map(async(r,o)=>{await r.init(),o!=n.length-1&&(t.lastDiskOffset+=r.size),t.size+=r.size})),super.init()}async readUint8Array(t,n,r=0){const o=this,{readers:s}=this;let i,l=r;l==-1&&(l=s.length-1);let c=t;for(;c>=s[l].size;)c-=s[l].size,l++;const a=s[l],p=a.size;if(c+n<=p)i=await he(a,c,n);else{const m=p-c;i=new Uint8Array(n),i.set(await he(a,c,m)),i.set(await o.readUint8Array(t+m,n-m,r),m)}return o.lastDiskNumber=Math.max(l,o.lastDiskNumber),i}}class Ko extends ks{constructor(t,n=4294967295){super();const r=this;Object.assign(r,{diskNumber:0,diskOffset:0,size:0,maxSize:n,availableSize:n});let o,s,i;const l=new WritableStream({async write(p){const{availableSize:m}=r;if(i)p.length>=m?(await c(p.slice(0,m)),await a(),r.diskOffset+=o.size,r.diskNumber++,i=null,await this.write(p.slice(m))):await c(p);else{const{value:h,done:x}=await t.next();if(x&&!h)throw new Error(Ff);o=h,o.size=0,o.maxSize&&(r.maxSize=o.maxSize),r.availableSize=r.maxSize,await Nr(o),s=h.writable,i=s.getWriter(),await this.write(p)}},async close(){await i.ready,await a()}});Object.defineProperty(r,ql,{get(){return l}});async function c(p){const m=p.length;m&&(await i.ready,await i.write(p),o.size+=m,r.size+=m,r.availableSize-=m)}async function a(){s.size=o.size,await i.close()}}}function dm(e){const{baseURL:t}=mf(),{protocol:n}=new URL(e,t);return n=="http:"||n=="https:"}async function Nr(e,t){e.init&&!e.initialized&&await e.init(t)}function zf(e){return Array.isArray(e)&&(e=new Wl(e)),e instanceof ReadableStream&&(e={readable:e}),e}function Xf(e){e.writable===Ge&&typeof e.next==hf&&(e=new Ko(e)),e instanceof WritableStream&&(e={writable:e});const{writable:t}=e;return t.size===Ge&&(t.size=0),e instanceof Ko||Object.assign(e,{diskNumber:0,diskOffset:0,availableSize:1/0,maxSize:1/0}),e}function he(e,t,n,r){return e.readUint8Array(t,n,r)}const pm=Wl,hm=Ko,Kf="\0☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ ".split(""),gm=Kf.length==256;function mm(e){if(gm){let t="";for(let n=0;n<e.length;n++)t+=Kf[e[n]];return t}else return new TextDecoder().decode(e)}function bi(e,t){return t&&t.trim().toLowerCase()=="cp437"?mm(e):new TextDecoder(t).decode(e)}const Zf="filename",Jf="rawFilename",_f="comment",$f="rawComment",ed="uncompressedSize",td="compressedSize",nd="offset",Ni="diskNumberStart",Oi="lastModDate",Pi="rawLastModDate",rd="lastAccessDate",vm="rawLastAccessDate",od="creationDate",wm="rawCreationDate",ym="internalFileAttribute",Am="externalFileAttribute",Em="msDosCompatible",xm="zip64",Sm=[Zf,Jf,td,ed,Oi,Pi,_f,$f,rd,od,nd,Ni,Ni,ym,Am,Em,xm,"directory","bitFlag","encrypted","signature","filenameUTF8","commentUTF8","compressionMethod","version","versionMadeBy","extraField","rawExtraField","extraFieldZip64","extraFieldUnicodePath","extraFieldUnicodeComment","extraFieldAES","extraFieldNTFS","extraFieldExtendedTimestamp"];class Oa{constructor(t){Sm.forEach(n=>this[n]=t[n])}}const Oo="File format is not recognized",sd="End of central directory not found",id="End of Zip64 central directory not found",ld="End of Zip64 central directory locator not found",cd="Central directory header not found",ad="Local file header not found",ud="Zip64 extra field not found",fd="File contains encrypted entry",dd="Encryption method not supported",Li="Compression method not supported",Mi="Split zip file",Pa="utf-8",La="cp437",km=[[ed,un],[td,un],[nd,un],[Ni,Lt]],Cm={[Lt]:{getValue:oe,bytes:4},[un]:{getValue:Po,bytes:8}};class Dm{constructor(t,n={}){Object.assign(this,{reader:zf(t),options:n,config:mf()})}async*getEntriesGenerator(t={}){const n=this;let{reader:r}=n;const{config:o}=n;if(await Nr(r),(r.size===Ge||!r.readUint8Array)&&(r=new jl(await new Response(r.readable).blob()),await Nr(r)),r.size<ao)throw new Error(Oo);r.chunkSize=ig(o);const s=await Om(r,Yh,r.size,ao,Lt*16);if(!s){const D=await he(r,0,4),T=fe(D);throw oe(T)==Gh?new Error(Mi):new Error(sd)}const i=fe(s);let l=oe(i,12),c=oe(i,16);const a=s.offset,p=ue(i,20),m=a+ao+p;let h=ue(i,4);const x=r.lastDiskNumber||0;let E=ue(i,6),v=ue(i,8),g=0,u=0;if(c==un||l==un||v==Lt||E==Lt){const D=await he(r,s.offset-zs,zs),T=fe(D);if(oe(T,0)!=zh)throw new Error(id);c=Po(T,8);let M=await he(r,c,Xs,-1),O=fe(M);const q=s.offset-zs-Xs;if(oe(O,0)!=pa&&c!=q){const U=c;c=q,g=c-U,M=await he(r,c,Xs,-1),O=fe(M)}if(oe(O,0)!=pa)throw new Error(ld);h==Lt&&(h=oe(O,16)),E==Lt&&(E=oe(O,20)),v==Lt&&(v=Po(O,32)),l==un&&(l=Po(O,40)),c-=l}if(x!=h)throw new Error(Mi);if(c<0||c>=r.size)throw new Error(Oo);let f=0,d=await he(r,c,l,E),y=fe(d);if(l){const D=s.offset-l;if(oe(y,f)!=da&&c!=D){const T=c;c=D,g=c-T,d=await he(r,c,l,E),y=fe(d)}}const k=s.offset-c-(r.lastDiskOffset||0);if(l!=k&&k>=0&&(l=k,d=await he(r,c,l,E),y=fe(d)),c<0||c>=r.size)throw new Error(Oo);const w=qe(n,t,"filenameEncoding"),S=qe(n,t,"commentEncoding");for(let D=0;D<v;D++){const T=new Rm(r,o,n.options);if(oe(y,f)!=da)throw new Error(cd);pd(T,y,f+6);const M=!!T.bitFlag.languageEncodingFlag,O=f+46,q=O+T.filenameLength,U=q+T.extraFieldLength,B=ue(y,f+4),Y=(B&0)==0,I=d.subarray(O,q),H=ue(y,f+32),N=U+H,z=d.subarray(U,N),F=M,G=M,st=Y&&(Qn(y,f+38)&va)==va,xn=oe(y,f+42)+g;Object.assign(T,{versionMadeBy:B,msDosCompatible:Y,compressedSize:0,uncompressedSize:0,commentLength:H,directory:st,offset:xn,diskNumberStart:ue(y,f+34),internalFileAttribute:ue(y,f+36),externalFileAttribute:oe(y,f+38),rawFilename:I,filenameUTF8:F,commentUTF8:G,rawExtraField:d.subarray(q,U)});const[it,Sn]=await Promise.all([bi(I,F?Pa:w||La),bi(z,G?Pa:S||La)]);Object.assign(T,{rawComment:z,filename:it,comment:Sn,directory:st||it.endsWith(rg)}),u=Math.max(xn,u),await hd(T,T,y,f+6);const js=new Oa(T);js.getData=(jc,Z0)=>T.getData(jc,js,Z0),f=N;const{onprogress:Qc}=t;if(Qc)try{await Qc(D+1,v,new Oa(T))}catch{}yield js}const C=qe(n,t,"extractPrependedData"),R=qe(n,t,"extractAppendedData");return C&&(n.prependedData=u>0?await he(r,0,u):new Uint8Array),n.comment=p?await he(r,a+ao,p):new Uint8Array,R&&(n.appendedData=m<r.size?await he(r,m,r.size-m):new Uint8Array),!0}async getEntries(t={}){const n=[];for await(const r of this.getEntriesGenerator(t))n.push(r);return n}async close(){}}class Rm{constructor(t,n,r){Object.assign(this,{reader:t,config:n,options:r})}async getData(t,n,r={}){const o=this,{reader:s,offset:i,diskNumberStart:l,extraFieldAES:c,compressionMethod:a,config:p,bitFlag:m,signature:h,rawLastModDate:x,uncompressedSize:E,compressedSize:v}=o,g=n.localDirectory={},u=await he(s,i,30,l),f=fe(u);let d=qe(o,r,"password");if(d=d&&d.length&&d,c&&c.originalCompressionMethod!=Vh)throw new Error(Li);if(a!=jh&&a!=Qh)throw new Error(Li);if(oe(f,0)!=Wh)throw new Error(ad);pd(g,f,4),g.rawExtraField=g.extraFieldLength?await he(s,i+30+g.filenameLength,g.extraFieldLength,l):new Uint8Array,await hd(o,g,f,4,!0),Object.assign(n,{lastAccessDate:g.lastAccessDate,creationDate:g.creationDate});const y=o.encrypted&&g.encrypted,k=y&&!c;if(y){if(!k&&c.strength===Ge)throw new Error(dd);if(!d)throw new Error(fd)}const w=i+30+g.filenameLength+g.extraFieldLength,S=v,C=s.readable;Object.assign(C,{diskNumberStart:l,offset:w,size:S});const R=qe(o,r,"signal"),D=qe(o,r,"checkPasswordOnly");D&&(t=new WritableStream),t=Xf(t),await Nr(t,E);const{writable:T}=t,{onstart:M,onprogress:O,onend:q}=r,U={options:{codecType:Bf,password:d,zipCrypto:k,encryptionStrength:c&&c.strength,signed:qe(o,r,"checkSignature"),passwordVerification:k&&(m.dataDescriptor?x>>>8&255:h>>>24&255),signature:h,compressed:a!=0,encrypted:y,useWebWorkers:qe(o,r,"useWebWorkers"),useCompressionStream:qe(o,r,"useCompressionStream"),transferStreams:qe(o,r,"transferStreams"),checkPasswordOnly:D},config:p,streamOptions:{signal:R,size:S,onstart:M,onprogress:O,onend:q}};let B=0;try{({outputSize:B}=await zg({readable:C,writable:T},U))}catch(Y){if(!D||Y.message!=Bl)throw Y}finally{const Y=qe(o,r,"preventClose");T.size+=B,!Y&&!T.locked&&await T.getWriter().close()}return D?void 0:t.getData?t.getData():T}}function pd(e,t,n){const r=e.rawBitFlag=ue(t,n+2),o=(r&ha)==ha,s=oe(t,n+6);Object.assign(e,{encrypted:o,version:ue(t,n),bitFlag:{level:(r&ng)>>1,dataDescriptor:(r&ga)==ga,languageEncodingFlag:(r&ma)==ma},rawLastModDate:s,lastModDate:Pm(s),filenameLength:ue(t,n+22),extraFieldLength:ue(t,n+24)})}async function hd(e,t,n,r,o){const{rawExtraField:s}=t,i=t.extraField=new Map,l=fe(new Uint8Array(s));let c=0;try{for(;c<s.length;){const u=ue(l,c),f=ue(l,c+2);i.set(u,{type:u,data:s.slice(c+4,c+4+f)}),c+=4+f}}catch{}const a=ue(n,r+4);Object.assign(t,{signature:oe(n,r+10),uncompressedSize:oe(n,r+18),compressedSize:oe(n,r+14)});const p=i.get(Xh);p&&(Tm(p,t),t.extraFieldZip64=p);const m=i.get($h);m&&(await Ma(m,Zf,Jf,t,e),t.extraFieldUnicodePath=m);const h=i.get(eg);h&&(await Ma(h,_f,$f,t,e),t.extraFieldUnicodeComment=h);const x=i.get(Kh);x?(Im(x,t,a),t.extraFieldAES=x):t.compressionMethod=a;const E=i.get(Zh);E&&(bm(E,t),t.extraFieldNTFS=E);const v=i.get(_h);v&&(Nm(v,t,o),t.extraFieldExtendedTimestamp=v);const g=i.get(tg);g&&(t.extraFieldUSDZ=g)}function Tm(e,t){t.zip64=!0;const n=fe(e.data),r=km.filter(([o,s])=>t[o]==s);for(let o=0,s=0;o<r.length;o++){const[i,l]=r[o];if(t[i]==l){const c=Cm[l];t[i]=e[i]=c.getValue(n,s),s+=c.bytes}else if(e[i])throw new Error(ud)}}async function Ma(e,t,n,r,o){const s=fe(e.data),i=new zo;i.append(o[n]);const l=fe(new Uint8Array(4));l.setUint32(0,i.get(),!0);const c=oe(s,1);Object.assign(e,{version:Qn(s,0),[t]:bi(e.data.subarray(5)),valid:!o.bitFlag.languageEncodingFlag&&c==oe(l,0)}),e.valid&&(r[t]=e[t],r[t+"UTF8"]=!0)}function Im(e,t,n){const r=fe(e.data),o=Qn(r,4);Object.assign(e,{vendorVersion:Qn(r,0),vendorId:Qn(r,2),strength:o,originalCompressionMethod:n,compressionMethod:ue(r,5)}),t.compressionMethod=e.compressionMethod}function bm(e,t){const n=fe(e.data);let r=4,o;try{for(;r<e.data.length&&!o;){const s=ue(n,r),i=ue(n,r+2);s==Jh&&(o=e.data.slice(r+4,r+4+i)),r+=4+i}}catch{}try{if(o&&o.length==24){const s=fe(o),i=s.getBigUint64(0,!0),l=s.getBigUint64(8,!0),c=s.getBigUint64(16,!0);Object.assign(e,{rawLastModDate:i,rawLastAccessDate:l,rawCreationDate:c});const a=_s(i),p=_s(l),m=_s(c),h={lastModDate:a,lastAccessDate:p,creationDate:m};Object.assign(e,h),Object.assign(t,h)}}catch{}}function Nm(e,t,n){const r=fe(e.data),o=Qn(r,0),s=[],i=[];n?((o&1)==1&&(s.push(Oi),i.push(Pi)),(o&2)==2&&(s.push(rd),i.push(vm)),(o&4)==4&&(s.push(od),i.push(wm))):e.data.length>=5&&(s.push(Oi),i.push(Pi));let l=1;s.forEach((c,a)=>{if(e.data.length>=l+4){const p=oe(r,l);t[c]=e[c]=new Date(p*1e3);const m=i[a];e[m]=p}l+=4})}async function Om(e,t,n,r,o){const s=new Uint8Array(4),i=fe(s);Lm(i,0,t);const l=r+o;return await c(r)||await c(Math.min(l,n));async function c(a){const p=n-a,m=await he(e,p,a);for(let h=m.length-r;h>=0;h--)if(m[h]==s[0]&&m[h+1]==s[1]&&m[h+2]==s[2]&&m[h+3]==s[3])return{offset:p+h,buffer:m.slice(h,h+r).buffer}}}function qe(e,t,n){return t[n]===Ge?e.options[n]:t[n]}function Pm(e){const t=(e&4294901760)>>16,n=e&65535;try{return new Date(1980+((t&65024)>>9),((t&480)>>5)-1,t&31,(n&63488)>>11,(n&2016)>>5,(n&31)*2,0)}catch{}}function _s(e){return new Date(Number(e/BigInt(1e4)-BigInt(116444736e5)))}function Qn(e,t){return e.getUint8(t)}function ue(e,t){return e.getUint16(t,!0)}function oe(e,t){return e.getUint32(t,!0)}function Po(e,t){return Number(e.getBigUint64(t,!0))}function Lm(e,t,n){e.setUint32(t,n,!0)}function fe(e){return new DataView(e.buffer)}vf({Inflate:qh});const Mm=Object.freeze(Object.defineProperty({__proto__:null,BlobReader:jl,BlobWriter:qf,Data64URIReader:rm,Data64URIWriter:om,ERR_BAD_FORMAT:Oo,ERR_CENTRAL_DIRECTORY_NOT_FOUND:cd,ERR_ENCRYPTED:fd,ERR_EOCDR_LOCATOR_ZIP64_NOT_FOUND:ld,ERR_EOCDR_NOT_FOUND:sd,ERR_EOCDR_ZIP64_NOT_FOUND:id,ERR_EXTRAFIELD_ZIP64_NOT_FOUND:ud,ERR_HTTP_RANGE:$r,ERR_INVALID_PASSWORD:Ll,ERR_INVALID_SIGNATURE:Ml,ERR_ITERATOR_COMPLETED_TOO_SOON:Ff,ERR_LOCAL_FILE_HEADER_NOT_FOUND:ad,ERR_SPLIT_ZIP_FILE:Mi,ERR_UNSUPPORTED_COMPRESSION:Li,ERR_UNSUPPORTED_ENCRYPTION:dd,HttpRangeReader:am,HttpReader:Yf,Reader:_t,SplitDataReader:Wl,SplitDataWriter:Ko,SplitZipReader:pm,SplitZipWriter:hm,TextReader:sm,TextWriter:im,Uint8ArrayReader:um,Uint8ArrayWriter:fm,Writer:Ql,ZipReader:Dm,configure:vf,getMimeType:lg,initReader:zf,initStream:Nr,initWriter:Xf,readUint8Array:he,terminateWorkers:Xg},Symbol.toStringTag,{value:"Module"}));var gd={exports:{}},Fe={},md={exports:{}},vd={};/**
 * @license React
 * scheduler.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */(function(e){function t(I,H){var N=I.length;I.push(H);e:for(;0<N;){var z=N-1>>>1,F=I[z];if(0<o(F,H))I[z]=H,I[N]=F,N=z;else break e}}function n(I){return I.length===0?null:I[0]}function r(I){if(I.length===0)return null;var H=I[0],N=I.pop();if(N!==H){I[0]=N;e:for(var z=0,F=I.length,G=F>>>1;z<G;){var st=2*(z+1)-1,xn=I[st],it=st+1,Sn=I[it];if(0>o(xn,N))it<F&&0>o(Sn,xn)?(I[z]=Sn,I[it]=N,z=it):(I[z]=xn,I[st]=N,z=st);else if(it<F&&0>o(Sn,N))I[z]=Sn,I[it]=N,z=it;else break e}}return H}function o(I,H){var N=I.sortIndex-H.sortIndex;return N!==0?N:I.id-H.id}if(typeof performance=="object"&&typeof performance.now=="function"){var s=performance;e.unstable_now=function(){return s.now()}}else{var i=Date,l=i.now();e.unstable_now=function(){return i.now()-l}}var c=[],a=[],p=1,m=null,h=3,x=!1,E=!1,v=!1,g=typeof setTimeout=="function"?setTimeout:null,u=typeof clearTimeout=="function"?clearTimeout:null,f=typeof setImmediate<"u"?setImmediate:null;typeof navigator<"u"&&navigator.scheduling!==void 0&&navigator.scheduling.isInputPending!==void 0&&navigator.scheduling.isInputPending.bind(navigator.scheduling);function d(I){for(var H=n(a);H!==null;){if(H.callback===null)r(a);else if(H.startTime<=I)r(a),H.sortIndex=H.expirationTime,t(c,H);else break;H=n(a)}}function y(I){if(v=!1,d(I),!E)if(n(c)!==null)E=!0,B(k);else{var H=n(a);H!==null&&Y(y,H.startTime-I)}}function k(I,H){E=!1,v&&(v=!1,u(C),C=-1),x=!0;var N=h;try{for(d(H),m=n(c);m!==null&&(!(m.expirationTime>H)||I&&!T());){var z=m.callback;if(typeof z=="function"){m.callback=null,h=m.priorityLevel;var F=z(m.expirationTime<=H);H=e.unstable_now(),typeof F=="function"?m.callback=F:m===n(c)&&r(c),d(H)}else r(c);m=n(c)}if(m!==null)var G=!0;else{var st=n(a);st!==null&&Y(y,st.startTime-H),G=!1}return G}finally{m=null,h=N,x=!1}}var w=!1,S=null,C=-1,R=5,D=-1;function T(){return!(e.unstable_now()-D<R)}function M(){if(S!==null){var I=e.unstable_now();D=I;var H=!0;try{H=S(!0,I)}finally{H?O():(w=!1,S=null)}}else w=!1}var O;if(typeof f=="function")O=function(){f(M)};else if(typeof MessageChannel<"u"){var q=new MessageChannel,U=q.port2;q.port1.onmessage=M,O=function(){U.postMessage(null)}}else O=function(){g(M,0)};function B(I){S=I,w||(w=!0,O())}function Y(I,H){C=g(function(){I(e.unstable_now())},H)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(I){I.callback=null},e.unstable_continueExecution=function(){E||x||(E=!0,B(k))},e.unstable_forceFrameRate=function(I){0>I||125<I?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):R=0<I?Math.floor(1e3/I):5},e.unstable_getCurrentPriorityLevel=function(){return h},e.unstable_getFirstCallbackNode=function(){return n(c)},e.unstable_next=function(I){switch(h){case 1:case 2:case 3:var H=3;break;default:H=h}var N=h;h=H;try{return I()}finally{h=N}},e.unstable_pauseExecution=function(){},e.unstable_requestPaint=function(){},e.unstable_runWithPriority=function(I,H){switch(I){case 1:case 2:case 3:case 4:case 5:break;default:I=3}var N=h;h=I;try{return H()}finally{h=N}},e.unstable_scheduleCallback=function(I,H,N){var z=e.unstable_now();switch(typeof N=="object"&&N!==null?(N=N.delay,N=typeof N=="number"&&0<N?z+N:z):N=z,I){case 1:var F=-1;break;case 2:F=250;break;case 5:F=1073741823;break;case 4:F=1e4;break;default:F=5e3}return F=N+F,I={id:p++,callback:H,priorityLevel:I,startTime:N,expirationTime:F,sortIndex:-1},N>z?(I.sortIndex=N,t(a,I),n(c)===null&&I===n(a)&&(v?(u(C),C=-1):v=!0,Y(y,N-z))):(I.sortIndex=F,t(c,I),E||x||(E=!0,B(k))),I},e.unstable_shouldYield=T,e.unstable_wrapCallback=function(I){var H=h;return function(){var N=h;h=H;try{return I.apply(this,arguments)}finally{h=N}}}})(vd);md.exports=vd;var Bm=md.exports;/**
 * @license React
 * react-dom.production.min.js
 *
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */var wd=j,He=Bm;function b(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n<arguments.length;n++)t+="&args[]="+encodeURIComponent(arguments[n]);return"Minified React error #"+e+"; visit "+t+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}var yd=new Set,Or={};function An(e,t){Kn(e,t),Kn(e+"Capture",t)}function Kn(e,t){for(Or[e]=t,e=0;e<t.length;e++)yd.add(t[e])}var St=!(typeof window>"u"||typeof window.document>"u"||typeof window.document.createElement>"u"),Bi=Object.prototype.hasOwnProperty,Hm=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,Ba={},Ha={};function Fm(e){return Bi.call(Ha,e)?!0:Bi.call(Ba,e)?!1:Hm.test(e)?Ha[e]=!0:(Ba[e]=!0,!1)}function Um(e,t,n,r){if(n!==null&&n.type===0)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return r?!1:n!==null?!n.acceptsBooleans:(e=e.toLowerCase().slice(0,5),e!=="data-"&&e!=="aria-");default:return!1}}function qm(e,t,n,r){if(t===null||typeof t>"u"||Um(e,t,n,r))return!0;if(r)return!1;if(n!==null)switch(n.type){case 3:return!t;case 4:return t===!1;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}function Ce(e,t,n,r,o,s,i){this.acceptsBooleans=t===2||t===3||t===4,this.attributeName=r,this.attributeNamespace=o,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=s,this.removeEmptyString=i}var me={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach(function(e){me[e]=new Ce(e,0,!1,e,null,!1,!1)});[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach(function(e){var t=e[0];me[t]=new Ce(t,1,!1,e[1],null,!1,!1)});["contentEditable","draggable","spellCheck","value"].forEach(function(e){me[e]=new Ce(e,2,!1,e.toLowerCase(),null,!1,!1)});["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach(function(e){me[e]=new Ce(e,2,!1,e,null,!1,!1)});"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach(function(e){me[e]=new Ce(e,3,!1,e.toLowerCase(),null,!1,!1)});["checked","multiple","muted","selected"].forEach(function(e){me[e]=new Ce(e,3,!0,e,null,!1,!1)});["capture","download"].forEach(function(e){me[e]=new Ce(e,4,!1,e,null,!1,!1)});["cols","rows","size","span"].forEach(function(e){me[e]=new Ce(e,6,!1,e,null,!1,!1)});["rowSpan","start"].forEach(function(e){me[e]=new Ce(e,5,!1,e.toLowerCase(),null,!1,!1)});var Gl=/[\-:]([a-z])/g;function Yl(e){return e[1].toUpperCase()}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach(function(e){var t=e.replace(Gl,Yl);me[t]=new Ce(t,1,!1,e,null,!1,!1)});"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach(function(e){var t=e.replace(Gl,Yl);me[t]=new Ce(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)});["xml:base","xml:lang","xml:space"].forEach(function(e){var t=e.replace(Gl,Yl);me[t]=new Ce(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)});["tabIndex","crossOrigin"].forEach(function(e){me[e]=new Ce(e,1,!1,e.toLowerCase(),null,!1,!1)});me.xlinkHref=new Ce("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1);["src","href","action","formAction"].forEach(function(e){me[e]=new Ce(e,1,!1,e.toLowerCase(),null,!0,!0)});function zl(e,t,n,r){var o=me.hasOwnProperty(t)?me[t]:null;(o!==null?o.type!==0:r||!(2<t.length)||t[0]!=="o"&&t[0]!=="O"||t[1]!=="n"&&t[1]!=="N")&&(qm(t,n,o,r)&&(n=null),r||o===null?Fm(t)&&(n===null?e.removeAttribute(t):e.setAttribute(t,""+n)):o.mustUseProperty?e[o.propertyName]=n===null?o.type===3?!1:"":n:(t=o.attributeName,r=o.attributeNamespace,n===null?e.removeAttribute(t):(o=o.type,n=o===3||o===4&&n===!0?"":""+n,r?e.setAttributeNS(r,t,n):e.setAttribute(t,n))))}var Dt=wd.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,uo=Symbol.for("react.element"),Tn=Symbol.for("react.portal"),In=Symbol.for("react.fragment"),Xl=Symbol.for("react.strict_mode"),Hi=Symbol.for("react.profiler"),Ad=Symbol.for("react.provider"),Ed=Symbol.for("react.context"),Kl=Symbol.for("react.forward_ref"),Fi=Symbol.for("react.suspense"),Ui=Symbol.for("react.suspense_list"),Zl=Symbol.for("react.memo"),Bt=Symbol.for("react.lazy"),xd=Symbol.for("react.offscreen"),Fa=Symbol.iterator;function ir(e){return e===null||typeof e!="object"?null:(e=Fa&&e[Fa]||e["@@iterator"],typeof e=="function"?e:null)}var ne=Object.assign,$s;function gr(e){if($s===void 0)try{throw Error()}catch(n){var t=n.stack.trim().match(/\n( *(at )?)/);$s=t&&t[1]||""}return`
`+$s+e}var ei=!1;function ti(e,t){if(!e||ei)return"";ei=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),typeof Reflect=="object"&&Reflect.construct){try{Reflect.construct(t,[])}catch(a){var r=a}Reflect.construct(e,[],t)}else{try{t.call()}catch(a){r=a}e.call(t.prototype)}else{try{throw Error()}catch(a){r=a}e()}}catch(a){if(a&&r&&typeof a.stack=="string"){for(var o=a.stack.split(`
`),s=r.stack.split(`
`),i=o.length-1,l=s.length-1;1<=i&&0<=l&&o[i]!==s[l];)l--;for(;1<=i&&0<=l;i--,l--)if(o[i]!==s[l]){if(i!==1||l!==1)do if(i--,l--,0>l||o[i]!==s[l]){var c=`
`+o[i].replace(" at new "," at ");return e.displayName&&c.includes("<anonymous>")&&(c=c.replace("<anonymous>",e.displayName)),c}while(1<=i&&0<=l);break}}}finally{ei=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?gr(e):""}function Qm(e){switch(e.tag){case 5:return gr(e.type);case 16:return gr("Lazy");case 13:return gr("Suspense");case 19:return gr("SuspenseList");case 0:case 2:case 15:return e=ti(e.type,!1),e;case 11:return e=ti(e.type.render,!1),e;case 1:return e=ti(e.type,!0),e;default:return""}}function qi(e){if(e==null)return null;if(typeof e=="function")return e.displayName||e.name||null;if(typeof e=="string")return e;switch(e){case In:return"Fragment";case Tn:return"Portal";case Hi:return"Profiler";case Xl:return"StrictMode";case Fi:return"Suspense";case Ui:return"SuspenseList"}if(typeof e=="object")switch(e.$$typeof){case Ed:return(e.displayName||"Context")+".Consumer";case Ad:return(e._context.displayName||"Context")+".Provider";case Kl:var t=e.render;return e=e.displayName,e||(e=t.displayName||t.name||"",e=e!==""?"ForwardRef("+e+")":"ForwardRef"),e;case Zl:return t=e.displayName||null,t!==null?t:qi(e.type)||"Memo";case Bt:t=e._payload,e=e._init;try{return qi(e(t))}catch{}}return null}function jm(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=t.render,e=e.displayName||e.name||"",t.displayName||(e!==""?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return qi(t);case 8:return t===Xl?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if(typeof t=="function")return t.displayName||t.name||null;if(typeof t=="string")return t}return null}function Kt(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":return e;case"object":return e;default:return""}}function Sd(e){var t=e.type;return(e=e.nodeName)&&e.toLowerCase()==="input"&&(t==="checkbox"||t==="radio")}function Vm(e){var t=Sd(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&typeof n<"u"&&typeof n.get=="function"&&typeof n.set=="function"){var o=n.get,s=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(i){r=""+i,s.call(this,i)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(i){r=""+i},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}function fo(e){e._valueTracker||(e._valueTracker=Vm(e))}function kd(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Sd(e)?e.checked?"true":"false":e.value),e=r,e!==n?(t.setValue(e),!0):!1}function Zo(e){if(e=e||(typeof document<"u"?document:void 0),typeof e>"u")return null;try{return e.activeElement||e.body}catch{return e.body}}function Qi(e,t){var n=t.checked;return ne({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:n??e._wrapperState.initialChecked})}function Ua(e,t){var n=t.defaultValue==null?"":t.defaultValue,r=t.checked!=null?t.checked:t.defaultChecked;n=Kt(t.value!=null?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:t.type==="checkbox"||t.type==="radio"?t.checked!=null:t.value!=null}}function Cd(e,t){t=t.checked,t!=null&&zl(e,"checked",t,!1)}function ji(e,t){Cd(e,t);var n=Kt(t.value),r=t.type;if(n!=null)r==="number"?(n===0&&e.value===""||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if(r==="submit"||r==="reset"){e.removeAttribute("value");return}t.hasOwnProperty("value")?Vi(e,t.type,n):t.hasOwnProperty("defaultValue")&&Vi(e,t.type,Kt(t.defaultValue)),t.checked==null&&t.defaultChecked!=null&&(e.defaultChecked=!!t.defaultChecked)}function qa(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!(r!=="submit"&&r!=="reset"||t.value!==void 0&&t.value!==null))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}n=e.name,n!==""&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,n!==""&&(e.name=n)}function Vi(e,t,n){(t!=="number"||Zo(e.ownerDocument)!==e)&&(n==null?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var mr=Array.isArray;function jn(e,t,n,r){if(e=e.options,t){t={};for(var o=0;o<n.length;o++)t["$"+n[o]]=!0;for(n=0;n<e.length;n++)o=t.hasOwnProperty("$"+e[n].value),e[n].selected!==o&&(e[n].selected=o),o&&r&&(e[n].defaultSelected=!0)}else{for(n=""+Kt(n),t=null,o=0;o<e.length;o++){if(e[o].value===n){e[o].selected=!0,r&&(e[o].defaultSelected=!0);return}t!==null||e[o].disabled||(t=e[o])}t!==null&&(t.selected=!0)}}function Wi(e,t){if(t.dangerouslySetInnerHTML!=null)throw Error(b(91));return ne({},t,{value:void 0,defaultValue:void 0,children:""+e._wrapperState.initialValue})}function Qa(e,t){var n=t.value;if(n==null){if(n=t.children,t=t.defaultValue,n!=null){if(t!=null)throw Error(b(92));if(mr(n)){if(1<n.length)throw Error(b(93));n=n[0]}t=n}t==null&&(t=""),n=t}e._wrapperState={initialValue:Kt(n)}}function Dd(e,t){var n=Kt(t.value),r=Kt(t.defaultValue);n!=null&&(n=""+n,n!==e.value&&(e.value=n),t.defaultValue==null&&e.defaultValue!==n&&(e.defaultValue=n)),r!=null&&(e.defaultValue=""+r)}function ja(e){var t=e.textContent;t===e._wrapperState.initialValue&&t!==""&&t!==null&&(e.value=t)}function Rd(e){switch(e){case"svg":return"http://www.w3.org/2000/svg";case"math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function Gi(e,t){return e==null||e==="http://www.w3.org/1999/xhtml"?Rd(t):e==="http://www.w3.org/2000/svg"&&t==="foreignObject"?"http://www.w3.org/1999/xhtml":e}var po,Td=function(e){return typeof MSApp<"u"&&MSApp.execUnsafeLocalFunction?function(t,n,r,o){MSApp.execUnsafeLocalFunction(function(){return e(t,n,r,o)})}:e}(function(e,t){if(e.namespaceURI!=="http://www.w3.org/2000/svg"||"innerHTML"in e)e.innerHTML=t;else{for(po=po||document.createElement("div"),po.innerHTML="<svg>"+t.valueOf().toString()+"</svg>",t=po.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appe
Download .txt
gitextract_gdn6le0m/

├── .devcontainer/
│   ├── devcontainer.json
│   └── postbuild
├── .gitattributes
├── .gitignore
├── .prettierrc
├── .vscode/
│   ├── settings.json
│   └── tasks.json
├── README.md
├── core/
│   ├── README.md
│   ├── package.json
│   ├── src/
│   │   ├── BotSecrets.ts
│   │   ├── CodeEvaluation.ts
│   │   ├── Cron.ts
│   │   ├── DataEncryption.ts
│   │   ├── DeviceTracking.ts
│   │   ├── ExpenseTracking.ts
│   │   ├── ExpenseTrackingGrist.ts
│   │   ├── HomeAutomation.ts
│   │   ├── ImageMessageHandler.ts
│   │   ├── LINEClient.ts
│   │   ├── LINEMessageUtilities.ts
│   │   ├── LanguageModelAssistant.ts
│   │   ├── MessageHandler.ts
│   │   ├── MessageHistory.ts
│   │   ├── MongoDatabase.ts
│   │   ├── NotificationProcessor.ts
│   │   ├── PersistentState.ts
│   │   ├── PhoneFinder.ts
│   │   ├── PreludeCode.ts
│   │   ├── RomanNumerals.ts
│   │   ├── SMSHandler.ts
│   │   ├── SlackMessageUtilities.ts
│   │   ├── SpeedDial.ts
│   │   ├── SpendingTracking.ts
│   │   ├── TemporaryBlobStorage.ts
│   │   ├── Tracing.ts
│   │   ├── TypedGristDocAPI.ts
│   │   ├── bot.ts
│   │   ├── logger.ts
│   │   ├── modules.d.ts
│   │   ├── scripts/
│   │   │   └── updateEnv.ts
│   │   ├── server.ts
│   │   ├── typedefs.d.ts
│   │   └── types.ts
│   └── tsconfig.json
├── fnox.toml
├── frpc.toml
├── images/
│   └── .gitkeep
├── mise.toml
├── package.json
├── pnpm-workspace.yaml
└── webui/
    ├── .eslintrc.cjs
    ├── .gitignore
    ├── README.md
    ├── app/
    │   ├── Clock.tsx
    │   ├── backend.ts
    │   ├── firebase.ts
    │   ├── index.css
    │   ├── requireAuth.ts
    │   ├── root.tsx
    │   └── routes/
    │       ├── _index.tsx
    │       ├── automatron._index.tsx
    │       ├── automatron.knobs.tsx
    │       └── automatron.tsx
    ├── env.d.ts
    ├── package.json
    ├── playwright-report/
    │   └── index.html
    ├── playwright.config.ts
    ├── postcss.config.cjs
    ├── tailwind.config.cjs
    ├── tests/
    │   └── automatron.spec.ts
    ├── tsconfig.json
    ├── vercel.json
    └── vite.config.ts
Download .txt
SYMBOL INDEX (167 symbols across 40 files)

FILE: core/src/BotSecrets.ts
  type BotSecrets (line 1) | interface BotSecrets {

FILE: core/src/CodeEvaluation.ts
  function evaluateCode (line 4) | async function evaluateCode(input: string, context: AutomatronContext) {
  function postProcessResult (line 27) | function postProcessResult(returnedValue: any) {

FILE: core/src/Cron.ts
  type CronEntry (line 5) | interface CronEntry {
  function addCronEntry (line 12) | async function addCronEntry(
  function getCronCollection (line 35) | async function getCronCollection(context: AutomatronContext) {
  function getPendingCronJobs (line 40) | async function getPendingCronJobs(context: AutomatronContext) {
  function updateCronJob (line 45) | async function updateCronJob(

FILE: core/src/DataEncryption.ts
  function decrypt (line 4) | function decrypt(
  function encrypt (line 12) | function encrypt(context: AutomatronContext, payload: any): string {

FILE: core/src/DeviceTracking.ts
  function getCollection (line 6) | async function getCollection(context: AutomatronContext) {
  type DeviceLogEntry (line 11) | interface DeviceLogEntry {
  function getDeviceStateUpdater (line 18) | async function getDeviceStateUpdater(
  function trackDevice (line 60) | async function trackDevice(
  function getDeviceRef (line 77) | function getDeviceRef(context: AutomatronContext, deviceId: string) {
  function checkDeviceOnlineStatus (line 81) | async function checkDeviceOnlineStatus(context: AutomatronContext) {
  function getDeviceIds (line 95) | async function getDeviceIds(context: AutomatronContext) {

FILE: core/src/ExpenseTracking.ts
  type ExpenseRecord (line 8) | type ExpenseRecord = ExpenseTrackingGristTables['Daily_Expenses']
  function getGristDoc (line 10) | function getGristDoc(context: AutomatronContext) {
  function recordExpense (line 18) | async function recordExpense(
  function getExpensesSummaryData (line 96) | async function getExpensesSummaryData(context: AutomatronContext) {

FILE: core/src/ExpenseTrackingGrist.ts
  type Text (line 2) | type Text = string
  type Numeric (line 3) | type Numeric = number
  type Int (line 4) | type Int = number
  type Bool (line 5) | type Bool = boolean
  type Date (line 6) | type Date = number
  type DateTime (line 7) | type DateTime = string
  type Choice (line 8) | type Choice = string
  type Reference (line 9) | type Reference = number
  type ReferenceList (line 10) | type ReferenceList = ['L', ...number[]] | null
  type ChoiceList (line 11) | type ChoiceList = ['L', ...string[]] | null
  type Attachments (line 12) | type Attachments = ['L', ...number[]] | null
  type Any (line 13) | type Any = any
  type ExpenseTrackingGristTables (line 16) | type ExpenseTrackingGristTables = {

FILE: core/src/HomeAutomation.ts
  function sendHomeCommand (line 5) | async function sendHomeCommand(

FILE: core/src/ImageMessageHandler.ts
  function annotateImage (line 28) | async function annotateImage(blobName: string) {

FILE: core/src/LINEClient.ts
  class LINEClient (line 3) | class LINEClient {
    method constructor (line 6) | constructor(config: { channelAccessToken: string }) {
    method replyMessage (line 14) | replyMessage(replyToken: string, messages: messagingApi.Message[]) {
    method pushMessage (line 17) | pushMessage(to: string, messages: messagingApi.Message[]) {
    method getMessageContent (line 20) | getMessageContent(messageId: string) {
    method showLoadingAnimation (line 23) | showLoadingAnimation(chatId: string) {

FILE: core/src/LINEMessageUtilities.ts
  function toMessages (line 3) | function toMessages(data: any): messagingApi.Message[] {
  function createBubble (line 10) | function createBubble(
  function truncate (line 76) | function truncate(text: string, maxLength: number) {

FILE: core/src/LanguageModelAssistant.ts
  type Role (line 12) | type Role = 'system' | 'user' | 'assistant'
  type ChatMessage (line 13) | interface ChatMessage {
  type LlmHistoryEntry (line 17) | interface LlmHistoryEntry {
  function getCollection (line 24) | async function getCollection(context: AutomatronContext) {

FILE: core/src/MessageHandler.ts
  function handleTextMessage (line 30) | async function handleTextMessage(
  function handleImage (line 142) | async function handleImage(

FILE: core/src/MessageHistory.ts
  type MessageHistoryEntry (line 4) | interface MessageHistoryEntry {
  function saveMessageHistory (line 10) | async function saveMessageHistory(
  function getMessageHistory (line 23) | async function getMessageHistory(

FILE: core/src/MongoDatabase.ts
  function getDb (line 14) | async function getDb(context: AutomatronContext): Promise<Db> {

FILE: core/src/NotificationProcessor.ts
  type INotification (line 5) | interface INotification {
  function handleNotification (line 15) | async function handleNotification(
  function saveNotificationToDb (line 40) | async function saveNotificationToDb(
  function handleKbankNotification (line 50) | async function handleKbankNotification(
  function parseAmount (line 93) | function parseAmount(text: string) {

FILE: core/src/PersistentState.ts
  type StateDoc (line 5) | interface StateDoc {
  type StateDocStack (line 10) | interface StateDocStack extends StateDoc {
  function push (line 14) | async function push(
  function pop (line 32) | async function pop(context: AutomatronContext, key: string): Promise<any> {
  function get (line 42) | async function get(context: AutomatronContext, key: string): Promise<any> {
  function set (line 50) | async function set(
  function ref (line 68) | function ref(context: AutomatronContext, key: string) {

FILE: core/src/PreludeCode.ts
  function deployPrelude (line 21) | async function deployPrelude(context: AutomatronContext) {
  function getPreludeCode (line 40) | async function getPreludeCode(context: AutomatronContext) {
  function getCodeExecutionContext (line 57) | async function getCodeExecutionContext(

FILE: core/src/RomanNumerals.ts
  function decodeRomanNumerals (line 1) | function decodeRomanNumerals(romanNumerals: string) {
  class InvalidRomanNumeralError (line 29) | class InvalidRomanNumeralError extends Error {
    method constructor (line 30) | constructor(s: string) {

FILE: core/src/SMSHandler.ts
  function handleSMS (line 7) | async function handleSMS(

FILE: core/src/SlackMessageUtilities.ts
  type SlackMessage (line 3) | type SlackMessage = SlackMessageWithoutBlocks | SlackMessageWithBlocks
  type SlackMessageWithoutBlocks (line 4) | type SlackMessageWithoutBlocks = {
  type SlackMessageWithBlocks (line 8) | type SlackMessageWithBlocks = { text?: string; blocks: KnownBlock[] }
  function createErrorMessage (line 10) | function createErrorMessage(error: Error): SlackMessage {

FILE: core/src/SpeedDial.ts
  function getCollection (line 4) | async function getCollection(context: AutomatronContext) {
  function saveSpeedDial (line 9) | async function saveSpeedDial(
  function getSpeedDialCode (line 22) | async function getSpeedDialCode(
  function getAllSpeedDials (line 34) | async function getAllSpeedDials(context: AutomatronContext) {

FILE: core/src/TemporaryBlobStorage.ts
  function putBlob (line 7) | async function putBlob(buffer: Buffer, extension: string) {
  function getBlob (line 14) | async function getBlob(blobName: string) {
  function getBlobUrl (line 22) | async function getBlobUrl(blobName: string) {

FILE: core/src/Tracing.ts
  function trace (line 3) | async function trace<T>(

FILE: core/src/TypedGristDocAPI.ts
  type TypedGristDocAPI (line 4) | interface TypedGristDocAPI<Tables extends AnyTables>
  type AnyTable (line 36) | type AnyTable = { [colId: string]: unknown }
  type AnyTables (line 37) | type AnyTables = { [table: string]: AnyTable }
  type FilterSpec (line 38) | type FilterSpec<Table extends AnyTable> = {

FILE: core/src/bot.ts
  function getAutomatronContext (line 34) | function getAutomatronContext(req: Request, res: Response): AutomatronCo...
  function runMiddleware (line 50) | async function runMiddleware(
  function handleWebhook (line 66) | async function handleWebhook(
  function logToSlack (line 292) | function logToSlack(
  function requireApiKey (line 436) | function requireApiKey(req: Request, res: Response, next: NextFunction) {
  function requireFirebaseAuth (line 450) | function requireFirebaseAuth(req: Request, res: Response, next: NextFunc...
  class Slack (line 463) | class Slack {
    method constructor (line 464) | constructor(private webhookUrl: string) {}
    method pushMessage (line 465) | async pushMessage(message: SlackMessage) {
  type ThirdPartyServices (line 470) | interface ThirdPartyServices {
  function endpoint (line 476) | function endpoint(
  function handleRequest (line 490) | async function handleRequest(
  function logError (line 526) | function logError(title: string, e: any, extra: Record<string, any> = {}) {
  function getLineConfig (line 536) | function getLineConfig(req: Request, res: Response) {
  function readAsBuffer (line 549) | function readAsBuffer(stream: Stream) {

FILE: core/src/logger.ts
  function getSeverity (line 8) | function getSeverity(label: string) {

FILE: core/src/modules.d.ts
  class ImageAnnotatorClient (line 2) | class ImageAnnotatorClient {
  type AirtableRecord (line 27) | interface AirtableRecord {
  class Airtable (line 31) | class Airtable {

FILE: core/src/scripts/updateEnv.ts
  method run (line 19) | async run({ args }) {
  function writeEnv (line 41) | async function writeEnv(key: string, encodedValue: string) {

FILE: core/src/server.ts
  constant PORT (line 13) | const PORT = process.env.PORT || 28364
  function loadEnv (line 20) | function loadEnv(): Promise<BotSecrets> {

FILE: core/src/types.ts
  type AutomatronContext (line 6) | interface AutomatronContext {
  type HttpRequestInfo (line 13) | interface HttpRequestInfo {
  type Request (line 21) | interface Request {
  type Response (line 25) | interface Response {
  type Global (line 30) | interface Global {
  type AutomatronResponse (line 38) | type AutomatronResponse =
  type TextMessageHandler (line 45) | type TextMessageHandler = (

FILE: webui/app/Clock.tsx
  function Clock (line 3) | function Clock() {

FILE: webui/app/backend.ts
  class AutomatronBackend (line 17) | class AutomatronBackend implements Backend {
    method constructor (line 22) | constructor() {
    method getUrl (line 34) | private async getUrl() {
    method signIn (line 45) | async signIn() {
    method signOut (line 55) | async signOut() {
    method send (line 59) | async send(text: string): Promise<any> {
    method getHeaders (line 72) | private async getHeaders() {
    method getHistory (line 78) | async getHistory(): Promise<any> {
    method getSpeedDials (line 82) | async getSpeedDials(): Promise<any> {
    method getKnobs (line 86) | async getKnobs(): Promise<Ok<{ knobs: Record<string, string> }>> {
    method getIdToken (line 90) | private async getIdToken() {
    method _get (line 94) | async _get(url: string) {
    method _post (line 101) | async _post(url: string, data: any) {
  class FakeBackend (line 109) | class FakeBackend implements Backend {
    method signIn (line 113) | async signIn() {
    method signOut (line 117) | async signOut() {
    method getHistory (line 121) | async getHistory(): Promise<any> {
    method getSpeedDials (line 125) | async getSpeedDials(): Promise<any> {
    method getKnobs (line 129) | async getKnobs(): Promise<Ok<{ knobs: Record<string, string> }>> {
    method send (line 133) | async send(text: string): Promise<any> {
  type Ok (line 138) | type Ok<X> = { ok: true; result: X }
  type Backend (line 140) | interface Backend {

FILE: webui/app/requireAuth.ts
  function requireAuth (line 4) | async function requireAuth() {

FILE: webui/app/root.tsx
  function App (line 12) | function App() {

FILE: webui/app/routes/_index.tsx
  function Index (line 9) | function Index() {
  function AuthButton (line 18) | function AuthButton() {
  type FloatingButton (line 48) | interface FloatingButton {
  function FloatingButton (line 54) | function FloatingButton(props: FloatingButton) {

FILE: webui/app/routes/automatron._index.tsx
  type ActionResult (line 28) | interface ActionResult {
  function AutomatronConsole (line 52) | function AutomatronConsole() {
  type OutputViewer (line 129) | interface OutputViewer {
  type OkResult (line 133) | type OkResult = z.infer<typeof OkResult>
  function OutputViewer (line 145) | function OutputViewer(props: OutputViewer) {
  type ResultEntry (line 165) | type ResultEntry = { type: 'text'; text: string }
  type ResultItem (line 167) | interface ResultItem {
  function ResultItem (line 171) | function ResultItem(props: ResultItem) {
  type AutomatronHistory (line 192) | interface AutomatronHistory {
  function AutomatronHistory (line 195) | function AutomatronHistory(props: AutomatronHistory) {
  type Panel (line 216) | interface Panel {
  function Panel (line 221) | function Panel(props: Panel) {
  type SpeedDial (line 232) | interface SpeedDial {
  function SpeedDial (line 235) | function SpeedDial(props: SpeedDial) {

FILE: webui/app/routes/automatron.knobs.tsx
  type AutomatronKnobs (line 7) | interface AutomatronKnobs {}
  function AutomatronKnobs (line 16) | function AutomatronKnobs() {

FILE: webui/app/routes/automatron.tsx
  function AutomatronLayout (line 4) | function AutomatronLayout() {

FILE: webui/tests/automatron.spec.ts
  function send (line 31) | async function send(page: Page, ...results: any[]) {
Condensed preview — 74 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (558K chars).
[
  {
    "path": ".devcontainer/devcontainer.json",
    "chars": 441,
    "preview": "// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:\n// https://github.co"
  },
  {
    "path": ".devcontainer/postbuild",
    "chars": 46,
    "preview": "#!/bin/bash -e\nnode core/dev set-up-codespaces"
  },
  {
    "path": ".gitattributes",
    "chars": 751,
    "preview": "# Don't allow people to merge changes to these generated files, because the result\r\n# may be invalid.  You need to run \""
  },
  {
    "path": ".gitignore",
    "chars": 208,
    "preview": ".env\n.cache\nnode_modules\n/automatron.js\n/automatron.js.map\n/dist/\n*.gz\nwebpack.stats.json\n*.env\n\n# Rush temporary files\n"
  },
  {
    "path": ".prettierrc",
    "chars": 43,
    "preview": "{\n  \"singleQuote\": true,\n  \"semi\": false\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 80,
    "preview": "{\n  \"cSpell.words\": [\"Automatron\"],\n  \"typescript.experimental.useTsgo\": true\n}\n"
  },
  {
    "path": ".vscode/tasks.json",
    "chars": 578,
    "preview": "{\n  \"version\": \"2.0.0\",\n  \"tasks\": [\n    {\n      \"label\": \"pnpm: dev\",\n      \"type\": \"shell\",\n      \"command\": \"pnpm dev"
  },
  {
    "path": "README.md",
    "chars": 3320,
    "preview": "# automatron\n\nThis is my personal LINE bot that helps me automate various tasks of everyday life, such as\n**home control"
  },
  {
    "path": "core/README.md",
    "chars": 722,
    "preview": "# @dtinth/automatron-core\n\nThe core service of automatron. Provides:\n\n- Chat bot interface (LINE, Slack)\n- REST API inte"
  },
  {
    "path": "core/package.json",
    "chars": 1808,
    "preview": "{\n  \"name\": \"@dtinth/automatron-core\",\n  \"private\": true,\n  \"description\": \"My LINE bot!\",\n  \"version\": \"1.0.0\",\n  \"lice"
  },
  {
    "path": "core/src/BotSecrets.ts",
    "chars": 1920,
    "preview": "export interface BotSecrets {\n  /** The secret key that must be sent with the request to use the [API](../README.md#cli-"
  },
  {
    "path": "core/src/CodeEvaluation.ts",
    "chars": 1702,
    "preview": "import { getCodeExecutionContext } from './PreludeCode'\nimport { AutomatronContext, TextMessageHandler } from './types'\n"
  },
  {
    "path": "core/src/Cron.ts",
    "chars": 1310,
    "preview": "import { ObjectId } from 'mongodb'\nimport { getDb } from './MongoDatabase'\nimport { AutomatronContext } from './types'\n\n"
  },
  {
    "path": "core/src/DataEncryption.ts",
    "chars": 462,
    "preview": "import Encrypted from '@dtinth/encrypted'\nimport { AutomatronContext } from './types'\n\nexport function decrypt(\n  contex"
  },
  {
    "path": "core/src/DeviceTracking.ts",
    "chars": 2638,
    "preview": "import { getDb } from './MongoDatabase'\nimport { ref } from './PersistentState'\nimport { AutomatronContext } from './typ"
  },
  {
    "path": "core/src/ExpenseTracking.ts",
    "chars": 3591,
    "preview": "import { messagingApi } from '@line/bot-sdk'\nimport { GristDocAPI } from 'grist-api'\nimport { ExpenseTrackingGristTables"
  },
  {
    "path": "core/src/ExpenseTrackingGrist.ts",
    "chars": 784,
    "preview": "export namespace grist {\n  export type Text = string\n  export type Numeric = number\n  export type Int = number\n  export "
  },
  {
    "path": "core/src/HomeAutomation.ts",
    "chars": 943,
    "preview": "import Encrypted from '@dtinth/encrypted'\nimport { AutomatronContext } from './types'\nimport axios from 'axios'\n\nexport "
  },
  {
    "path": "core/src/ImageMessageHandler.ts",
    "chars": 2011,
    "preview": "import vision from '@google-cloud/vision'\nimport { ref } from './PersistentState'\nimport { getBlob, getBlobUrl } from '."
  },
  {
    "path": "core/src/LINEClient.ts",
    "chars": 934,
    "preview": "import { messagingApi } from '@line/bot-sdk'\n\nexport class LINEClient {\n  private readonly api: messagingApi.MessagingAp"
  },
  {
    "path": "core/src/LINEMessageUtilities.ts",
    "chars": 1951,
    "preview": "import { messagingApi } from '@line/bot-sdk'\n\nexport function toMessages(data: any): messagingApi.Message[] {\n  if (!dat"
  },
  {
    "path": "core/src/LanguageModelAssistant.ts",
    "chars": 3304,
    "preview": "import axios from 'axios'\nimport { decrypt } from './DataEncryption'\nimport { logger } from './logger'\nimport { getDb } "
  },
  {
    "path": "core/src/MessageHandler.ts",
    "chars": 5387,
    "preview": "import { CodeEvaluationMessageHandler } from './CodeEvaluation'\nimport { addCronEntry } from './Cron'\nimport { recordExp"
  },
  {
    "path": "core/src/MessageHistory.ts",
    "chars": 1126,
    "preview": "import { getDb } from './MongoDatabase'\nimport { AutomatronContext, TextMessage, TextMessageHandler } from './types'\n\nex"
  },
  {
    "path": "core/src/MongoDatabase.ts",
    "chars": 792,
    "preview": "import { Db, MongoClient } from 'mongodb'\nimport { trace } from './Tracing'\nimport { AutomatronContext } from './types'\n"
  },
  {
    "path": "core/src/NotificationProcessor.ts",
    "chars": 2292,
    "preview": "import { getDb } from './MongoDatabase'\nimport { AutomatronContext } from './types'\nimport { logger } from './logger'\n\ne"
  },
  {
    "path": "core/src/PersistentState.ts",
    "chars": 1869,
    "preview": "import { getDb } from './MongoDatabase'\nimport { trace } from './Tracing'\nimport { AutomatronContext } from './types'\n\ni"
  },
  {
    "path": "core/src/PhoneFinder.ts",
    "chars": 808,
    "preview": "import { TextMessageHandler } from './types'\nimport { GoogleAuth } from 'google-auth-library'\nimport axios from 'axios'\n"
  },
  {
    "path": "core/src/PreludeCode.ts",
    "chars": 4594,
    "preview": "import axios from 'axios'\nimport { Storage } from '@google-cloud/storage'\nimport { AutomatronContext } from './types'\nim"
  },
  {
    "path": "core/src/RomanNumerals.ts",
    "chars": 1281,
    "preview": "export function decodeRomanNumerals(romanNumerals: string) {\n  const decode = (s: string): number => {\n    if (s.startsW"
  },
  {
    "path": "core/src/SMSHandler.ts",
    "chars": 2566,
    "preview": "import { messagingApi, QuickReplyItem } from '@line/bot-sdk'\nimport { AutomatronContext } from './types'\nimport { record"
  },
  {
    "path": "core/src/SlackMessageUtilities.ts",
    "chars": 787,
    "preview": "import { KnownBlock, MessageAttachment } from '@slack/types'\n\nexport type SlackMessage = SlackMessageWithoutBlocks | Sla"
  },
  {
    "path": "core/src/SpeedDial.ts",
    "chars": 971,
    "preview": "import { getDb } from './MongoDatabase'\nimport { AutomatronContext } from './types'\n\nasync function getCollection(contex"
  },
  {
    "path": "core/src/SpendingTracking.ts",
    "chars": 1472,
    "preview": "import { getDb } from './MongoDatabase'\nimport { ref } from './PersistentState'\nimport { TextMessageHandler } from './ty"
  },
  {
    "path": "core/src/TemporaryBlobStorage.ts",
    "chars": 920,
    "preview": "import { Storage } from '@google-cloud/storage'\nimport { nanoid } from 'nanoid'\n\nconst storage = new Storage()\nlet lates"
  },
  {
    "path": "core/src/Tracing.ts",
    "chars": 333,
    "preview": "import { AutomatronContext } from './types'\n\nexport async function trace<T>(\n  context: AutomatronContext,\n  name: strin"
  },
  {
    "path": "core/src/TypedGristDocAPI.ts",
    "chars": 1288,
    "preview": "import type { GristDocAPI } from 'grist-api'\n\n// From: https://dt.in.th/GristTypeGenerator\nexport interface TypedGristDo"
  },
  {
    "path": "core/src/bot.ts",
    "chars": 16003,
    "preview": "import Encrypted from '@dtinth/encrypted'\nimport { MessageEvent, middleware, WebhookEvent } from '@line/bot-sdk'\nimport "
  },
  {
    "path": "core/src/logger.ts",
    "chars": 723,
    "preview": "import pino from 'pino'\n\nexport const logger = pino({\n  // https://github.com/pinojs/pino/issues/726#issuecomment-605814"
  },
  {
    "path": "core/src/modules.d.ts",
    "chars": 1011,
    "preview": "declare module '@google-cloud/vision' {\n  export class ImageAnnotatorClient {\n    constructor(options?: any)\n    documen"
  },
  {
    "path": "core/src/scripts/updateEnv.ts",
    "chars": 1581,
    "preview": "import { Storage } from '@google-cloud/storage'\nimport { password } from '@inquirer/prompts'\nimport * as age from 'age-e"
  },
  {
    "path": "core/src/server.ts",
    "chars": 3078,
    "preview": "import { Storage } from '@google-cloud/storage'\nimport { start } from '@google-cloud/trace-agent'\nimport * as age from '"
  },
  {
    "path": "core/src/typedefs.d.ts",
    "chars": 61,
    "preview": "declare module 'tweetnacl-sealedbox-js'\ndeclare module 'lib'\n"
  },
  {
    "path": "core/src/types.ts",
    "chars": 1142,
    "preview": "import { FlexMessage, messagingApi, TextMessage } from '@line/bot-sdk'\nimport { BotSecrets } from './BotSecrets'\nimport "
  },
  {
    "path": "core/tsconfig.json",
    "chars": 220,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"es2020\",\n    \"module\": \"commonjs\",\n    \"noEmit\": true,\n    \"lib\": [\"es2020\"],\n  "
  },
  {
    "path": "fnox.toml",
    "chars": 1807,
    "preview": "default_provider = \"age\"\n\n[providers.age]\ntype = \"age\"\nrecipients = [\n  \"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHac2M/WdCs"
  },
  {
    "path": "frpc.toml",
    "chars": 158,
    "preview": "serverAddr = \"{{ .Envs.FRP_SERVER_ADDR }}\"\nserverPort = 7000\n\n[[proxies]]\nname = \"automatron\"\ntype = \"http\"\nlocalPort = "
  },
  {
    "path": "images/.gitkeep",
    "chars": 1,
    "preview": "\n"
  },
  {
    "path": "mise.toml",
    "chars": 59,
    "preview": "[tools]\nfnox = \"latest\"\nnode = \"24\"\n\n[env]\n_.file = '.env'\n"
  },
  {
    "path": "package.json",
    "chars": 458,
    "preview": "{\n  \"name\": \"automatron-workspace\",\n  \"private\": true,\n  \"packageManager\": \"pnpm@9.7.1+sha512.faf344af2d6ca65c4c5c8c2224"
  },
  {
    "path": "pnpm-workspace.yaml",
    "chars": 29,
    "preview": "packages:\n  - core\n  - webui\n"
  },
  {
    "path": "webui/.eslintrc.cjs",
    "chars": 2067,
    "preview": "/**\n * This is intended to be a basic starting point for linting in your app.\n * It relies on recommended configs out of"
  },
  {
    "path": "webui/.gitignore",
    "chars": 40,
    "preview": "node_modules\n\n/.cache\n/build\n.env\n/dist/"
  },
  {
    "path": "webui/README.md",
    "chars": 641,
    "preview": "# Welcome to Remix + Vite!\n\n📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/d"
  },
  {
    "path": "webui/app/Clock.tsx",
    "chars": 559,
    "preview": "import { useEffect, useState } from 'react'\n\nexport function Clock() {\n  const [time, setTime] = useState(getTime)\n  use"
  },
  {
    "path": "webui/app/backend.ts",
    "chars": 3760,
    "preview": "import {\n  getAuth,\n  onAuthStateChanged,\n  User,\n  GoogleAuthProvider,\n  signInWithPopup,\n  signOut as signOutFirebase,"
  },
  {
    "path": "webui/app/firebase.ts",
    "chars": 466,
    "preview": "import { initializeApp } from 'firebase/app'\n\n// Your web app's Firebase configuration\nconst firebaseConfig = {\n  apiKey"
  },
  {
    "path": "webui/app/index.css",
    "chars": 667,
    "preview": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\nbody {\n  font-family: Arimo, Helvetica, Arial, sans-serif;\n}"
  },
  {
    "path": "webui/app/requireAuth.ts",
    "chars": 221,
    "preview": "import { backend } from './backend'\nimport { redirect } from '@remix-run/react'\n\nexport async function requireAuth() {\n "
  },
  {
    "path": "webui/app/root.tsx",
    "chars": 540,
    "preview": "import {\n  Links,\n  LiveReload,\n  Meta,\n  Outlet,\n  Scripts,\n  ScrollRestoration,\n} from '@remix-run/react'\n\nimport './i"
  },
  {
    "path": "webui/app/routes/_index.tsx",
    "chars": 1460,
    "preview": "import { ReactNode, useSyncExternalStore } from 'react'\nimport { Clock } from '~/Clock'\nimport { backend } from '~/backe"
  },
  {
    "path": "webui/app/routes/automatron._index.tsx",
    "chars": 6862,
    "preview": "import { Icon } from '@iconify-icon/react'\nimport chevronLeft from '@iconify-icons/cil/chevron-left'\nimport chevronRight"
  },
  {
    "path": "webui/app/routes/automatron.knobs.tsx",
    "chars": 1621,
    "preview": "import { Icon } from '@iconify-icon/react'\nimport copy from '@iconify-icons/cil/copy'\nimport { useLoaderData } from '@re"
  },
  {
    "path": "webui/app/routes/automatron.tsx",
    "chars": 1069,
    "preview": "import { Link, Outlet, useLocation } from '@remix-run/react'\nimport { Fragment } from 'react'\n\nexport default function A"
  },
  {
    "path": "webui/env.d.ts",
    "chars": 80,
    "preview": "/// <reference types=\"@remix-run/node\" />\n/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "webui/package.json",
    "chars": 1581,
    "preview": "{\n  \"name\": \"webui-remix\",\n  \"private\": true,\n  \"sideEffects\": false,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"build\": \"r"
  },
  {
    "path": "webui/playwright-report/index.html",
    "chars": 426844,
    "preview": "\n\n<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset='UTF-8'>\n    <meta name='color-scheme' content='dark light'>\n    <me"
  },
  {
    "path": "webui/playwright.config.ts",
    "chars": 2674,
    "preview": "import type { PlaywrightTestConfig } from '@playwright/test'\nimport { devices } from '@playwright/test'\n\n/**\n * Read env"
  },
  {
    "path": "webui/postcss.config.cjs",
    "chars": 82,
    "preview": "module.exports = {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "webui/tailwind.config.cjs",
    "chars": 501,
    "preview": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n  content: ['./index.html', './app/**/*.{js,ts,jsx,tsx}']"
  },
  {
    "path": "webui/tests/automatron.spec.ts",
    "chars": 1391,
    "preview": "import { test, expect, Page } from '@playwright/test'\n\ntest('shows current time', async ({ page }) => {\n  await page.got"
  },
  {
    "path": "webui/tsconfig.json",
    "chars": 579,
    "preview": "{\n  \"include\": [\"env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n  \"compilerOptions\": {\n    \"lib\": [\"DOM\", \"DOM.Iterable\", \"ES2022\"],"
  },
  {
    "path": "webui/vercel.json",
    "chars": 100,
    "preview": "{\n  \"routes\": [\n    { \"handle\": \"filesystem\" },\n    { \"src\": \"/(.*)\", \"dest\": \"/index.html\" }\n  ]\n}\n"
  },
  {
    "path": "webui/vite.config.ts",
    "chars": 248,
    "preview": "import { vitePlugin as remix } from '@remix-run/dev'\nimport { defineConfig } from 'vite'\nimport tsconfigPaths from 'vite"
  }
]

About this extraction

This page contains the full source code of the dtinth/automatron GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 74 files (523.8 KB), approximately 226.5k tokens, and a symbol index with 167 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!