@alashi/cli 0.8.1 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/v1/chat.js +42 -2
- package/assets/v1/system.css +16 -18
- package/dist/cli.mjs +52 -4
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +230 -12
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/{store-Cxxnjq70.mjs → store-VoJe_U1Q.mjs} +736 -29
- package/dist/store-VoJe_U1Q.mjs.map +1 -0
- package/package.json +1 -1
- package/templates/basic-app/AGENTS.md +6 -1
- package/dist/store-Cxxnjq70.mjs.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-VoJe_U1Q.mjs","names":["execFileAsync","execFileAsync"],"sources":["../src/usage.ts","../src/budget.ts","../src/compat.ts","../src/loader.ts","../src/sources.ts","../src/registry.ts","../src/installer.ts","../src/portability.ts","../src/runtime.ts","../src/scheduler.ts","../src/settings-page.ts","../src/server.ts","../src/store.ts"],"sourcesContent":["import type { Store, UsageDay, UsageState } from './store.js';\n\n/** Days of history kept in usage.json; older days are dropped on write. */\nexport const USAGE_RETENTION_DAYS = 90;\n\n/** Cents-of-a-millicent precision — enough for per-turn costs, short enough to keep JSON tidy. */\nconst COST_PRECISION = 6;\n\nconst DAY_MS = 24 * 60 * 60 * 1000;\n\n/** The UTC calendar day (`YYYY-MM-DD`) a timestamp falls in. */\nexport function utcDay(at: Date = new Date()): string {\n return at.toISOString().slice(0, 10);\n}\n\nfunction round(value: number): number {\n return Number(value.toFixed(COST_PRECISION));\n}\n\n/** Money for humans: sub-dollar spend needs the extra digits, larger sums don't. */\nexport function formatUsd(value: number): string {\n return `$${value.toFixed(value >= 1 ? 2 : 4)}`;\n}\n\n/** A summary of an app's recorded spend, as served by GET /api/apps/:name/usage. */\nexport interface UsageSummary {\n /** The UTC day \"today\" refers to. */\n date: string;\n /** Spend recorded so far today. */\n todayCostUsd: number;\n /** Turns recorded so far today. */\n todayTurns: number;\n /** Spend across the whole retention window. */\n totalCostUsd: number;\n /** Days kept in the retention window. */\n retentionDays: number;\n /** Daily series, oldest first; days with no spend are absent. */\n days: UsageDay[];\n}\n\n/**\n * Per-app, per-UTC-day AI spend, persisted as OS bookkeeping in the app\n * container (`apps/<name>/usage.json`, next to jobs.json and chat.json).\n *\n * Cost arrives from the `result` event's `costUsd`, which the SDK contract\n * marks optional: engines that do not report cost simply contribute nothing.\n * Writes are serialized per app so concurrent turns cannot lose an update,\n * and each app has its own queue so a slow write for one app never delays\n * another.\n */\nexport class UsageRecorder {\n /** Serializes usage.json read-modify-write per app. */\n private readonly queues = new Map<string, Promise<unknown>>();\n\n constructor(\n private readonly store: Store,\n private readonly retentionDays: number = USAGE_RETENTION_DAYS,\n ) {}\n\n /**\n * Adds one turn's cost to the app's total for the UTC day of `at`.\n * A missing, non-finite, or negative `costUsd` is ignored (no turn counted,\n * no file written) so a provider that omits cost cannot corrupt the totals.\n */\n record(appName: string, costUsd: number | undefined, at: Date = new Date()): Promise<void> {\n if (typeof costUsd !== 'number' || !Number.isFinite(costUsd) || costUsd < 0) {\n return Promise.resolve();\n }\n return this.enqueue(appName, async () => {\n const state = await this.store.readUsageState(appName);\n const date = utcDay(at);\n const day = state.days.find((entry) => entry.date === date);\n if (day) {\n day.costUsd = round(day.costUsd + costUsd);\n day.turns += 1;\n } else {\n state.days.push({ date, costUsd: round(costUsd), turns: 1 });\n }\n await this.store.writeUsageState(appName, this.prune(state, at));\n });\n }\n\n /** The app's recorded days, oldest first, with expired days filtered out. */\n async read(appName: string, at: Date = new Date()): Promise<UsageState> {\n return this.prune(await this.store.readUsageState(appName), at);\n }\n\n /** Today's spend plus the daily series, for the dashboard and the usage endpoint. */\n async summary(appName: string, at: Date = new Date()): Promise<UsageSummary> {\n const { days } = await this.read(appName, at);\n const date = utcDay(at);\n const today = days.find((entry) => entry.date === date);\n return {\n date,\n todayCostUsd: today?.costUsd ?? 0,\n todayTurns: today?.turns ?? 0,\n totalCostUsd: round(days.reduce((sum, entry) => sum + entry.costUsd, 0)),\n retentionDays: this.retentionDays,\n days,\n };\n }\n\n /** Drops days outside the retention window and normalizes ordering/values. */\n private prune(state: UsageState, at: Date): UsageState {\n const cutoff = utcDay(new Date(at.getTime() - (this.retentionDays - 1) * DAY_MS));\n const days = state.days\n .filter(\n (entry) =>\n typeof entry?.date === 'string' &&\n entry.date >= cutoff &&\n Number.isFinite(entry.costUsd) &&\n entry.costUsd >= 0,\n )\n .map((entry) => ({\n date: entry.date,\n costUsd: round(entry.costUsd),\n turns: Number.isFinite(entry.turns) ? entry.turns : 0,\n }))\n .sort((a, b) => a.date.localeCompare(b.date));\n return { days };\n }\n\n private enqueue(appName: string, work: () => Promise<void>): Promise<void> {\n const queue = this.queues.get(appName) ?? Promise.resolve();\n const next = queue.then(work);\n // Keep the chain alive after a failed write, but surface the error to the caller.\n const settled = next.catch(() => undefined);\n this.queues.set(appName, settled);\n void settled.then(() => {\n if (this.queues.get(appName) === settled) this.queues.delete(appName);\n });\n return next;\n }\n}\n","import { AppSdkError } from '@alashi/apps-sdk';\nimport type { AppBudget, GlobalConfig, Store } from './store.js';\nimport { formatUsd, type UsageRecorder, type UsageSummary } from './usage.js';\n\n/**\n * Stable discriminator on the thrown error. Apps see the host's error object\n * across a package boundary (and, once `AppContext` becomes RPC, across a\n * process boundary), so the reliable check is this code — not `instanceof`.\n */\nexport const BUDGET_EXCEEDED_CODE = 'budget_exceeded';\n\n/** Today's spend for an app measured against its cap. */\nexport interface BudgetStatus {\n /** The UTC calendar day the numbers cover. */\n date: string;\n /** Spend recorded for that day. */\n spentUsd: number;\n /** The configured daily cap; absent when the app has none. */\n dailyUsd?: number;\n /** Spend left before sessions are refused; absent when there is no cap. */\n remainingUsd?: number;\n /** True only when a cap is set and the day's spend has reached it. */\n overBudget: boolean;\n}\n\n/**\n * Refusal from the `createSession` budget gate.\n *\n * Extends the SDK's `AppSdkError` so it stays inside the taxonomy apps already\n * catch, and carries a `code` plus the numbers behind the decision so an app\n * can degrade gracefully (queue the work, skip the run, tell the user) instead\n * of treating a spend cap as a crash. The message is written to stand alone:\n * it is often the only text a human sees, rendered somewhere far from the host.\n */\nexport class BudgetExceededError extends AppSdkError {\n readonly code: typeof BUDGET_EXCEEDED_CODE = BUDGET_EXCEEDED_CODE;\n readonly appName: string;\n /** UTC day the cap applies to. */\n readonly date: string;\n /** The configured cap in USD. */\n readonly limitUsd: number;\n /** Spend already recorded for `date`. */\n readonly spentUsd: number;\n\n constructor(appName: string, params: { date: string; limitUsd: number; spentUsd: number }) {\n super(\n `Daily AI spend cap reached: app \"${appName}\" has spent ${formatUsd(params.spentUsd)} of its ` +\n `${formatUsd(params.limitUsd)} cap for ${params.date} (UTC). New sessions are refused until ` +\n `the cap resets at 00:00 UTC, or until it is raised in the app's alashi settings.`,\n );\n this.name = 'BudgetExceededError';\n this.appName = appName;\n this.date = params.date;\n this.limitUsd = params.limitUsd;\n this.spentUsd = params.spentUsd;\n }\n\n /** Duck-typed check that survives duplicate copies of this module. */\n static is(err: unknown): err is BudgetExceededError {\n return (\n err instanceof Error && (err as { code?: unknown }).code === (BUDGET_EXCEEDED_CODE as string)\n );\n }\n}\n\n/**\n * The cap the *host* has set for an app, or undefined when there is none.\n *\n * Read only from `GlobalConfig.budgets`, which is deliberately not part of the\n * config layers merged into `ctx.config`: an app must not be able to raise its\n * own ceiling through its manifest defaults or `apps/<name>/config.json`.\n *\n * A hand-edited cap that is not a finite number >= 0 is ignored rather than\n * treated as zero — a typo in config.json should not brick an app, and the\n * dashboard/settings write path validates before it ever gets stored.\n */\nexport function appBudget(config: GlobalConfig, name: string): number | undefined {\n const dailyUsd = config.budgets?.[name]?.dailyUsd;\n if (typeof dailyUsd !== 'number' || !Number.isFinite(dailyUsd) || dailyUsd < 0) return undefined;\n return dailyUsd;\n}\n\n/** Whether `spentUsd` has reached `dailyUsd`; the cap itself is inclusive. */\nfunction isOver(spentUsd: number, dailyUsd: number | undefined): boolean {\n return dailyUsd !== undefined && spentUsd >= dailyUsd;\n}\n\n/** Combines an already-read usage summary with a cap; no I/O. */\nexport function budgetStatusFor(summary: UsageSummary, dailyUsd: number | undefined): BudgetStatus {\n return {\n date: summary.date,\n spentUsd: summary.todayCostUsd,\n ...(dailyUsd !== undefined && {\n dailyUsd,\n remainingUsd: Math.max(0, Number((dailyUsd - summary.todayCostUsd).toFixed(6))),\n }),\n overBudget: isOver(summary.todayCostUsd, dailyUsd),\n };\n}\n\n/** Today's spend against the app's cap, for the gate, the dashboard, and settings. */\nexport async function budgetStatus(\n store: Store,\n usage: UsageRecorder,\n name: string,\n at: Date = new Date(),\n): Promise<BudgetStatus> {\n const [config, summary] = await Promise.all([store.readGlobalConfig(), usage.summary(name, at)]);\n return budgetStatusFor(summary, appBudget(config, name));\n}\n\n/**\n * Fail-closed gate: throws `BudgetExceededError` when the app has already\n * spent its daily cap. Evaluated fresh on every call, so a cap edit or a\n * mid-day overspend takes effect without restarting the app.\n */\nexport async function assertWithinBudget(\n store: Store,\n usage: UsageRecorder,\n name: string,\n at: Date = new Date(),\n): Promise<void> {\n const status = await budgetStatus(store, usage, name, at);\n if (!status.overBudget || status.dailyUsd === undefined) return;\n throw new BudgetExceededError(name, {\n date: status.date,\n limitUsd: status.dailyUsd,\n spentUsd: status.spentUsd,\n });\n}\n\n/**\n * Sets (or, with `undefined`, clears) an app's daily cap in the host config.\n * Rejects anything that is not a finite, non-negative dollar amount; `0` is a\n * legitimate value meaning \"no AI spend at all for this app\".\n */\nexport async function setAppBudget(\n store: Store,\n name: string,\n dailyUsd: number | undefined,\n): Promise<void> {\n if (dailyUsd !== undefined && (!Number.isFinite(dailyUsd) || dailyUsd < 0)) {\n throw new AppSdkError(`Invalid daily spend cap: ${JSON.stringify(dailyUsd)}`);\n }\n const config = await store.readGlobalConfig();\n const budgets: Record<string, AppBudget> = { ...config.budgets };\n if (dailyUsd === undefined) delete budgets[name];\n else budgets[name] = { dailyUsd };\n const next: GlobalConfig = { ...config };\n if (Object.keys(budgets).length === 0) delete next.budgets;\n else next.budgets = budgets;\n await store.writeGlobalConfig(next);\n}\n","import { type AppManifest, AppSdkError, SDK_VERSION } from '@alashi/apps-sdk';\nimport semver from 'semver';\n\n/**\n * Enforces the manifest's declared `sdkVersion` range against the SDK\n * this host is built with. Apps that declare nothing are accepted.\n */\nexport function checkSdkCompatibility(manifest: Pick<AppManifest, 'name' | 'sdkVersion'>): void {\n const range = manifest.sdkVersion;\n if (!range) return;\n if (!semver.validRange(range)) {\n throw new AppSdkError(\n `App \"${manifest.name}\" declares an invalid sdkVersion range: ${JSON.stringify(range)}`,\n );\n }\n if (!semver.satisfies(SDK_VERSION, range)) {\n throw new AppSdkError(\n `App \"${manifest.name}\" supports SDK ${range}, but this host uses SDK ${SDK_VERSION}. ` +\n 'Update the host (or the app) to a compatible version.',\n );\n }\n}\n","import { readFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { type AppDefinition, type AppManifest, AppSdkError, parseManifest } from '@alashi/apps-sdk';\nimport { checkSdkCompatibility } from './compat.js';\n\ninterface PackageJson {\n main?: string;\n exports?: unknown;\n}\n\nfunction entryFromExports(exportsField: unknown): string | undefined {\n if (typeof exportsField === 'string') return exportsField;\n if (typeof exportsField !== 'object' || exportsField === null) return undefined;\n const record = exportsField as Record<string, unknown>;\n const dot = record['.'] ?? record.default ?? record.import;\n if (typeof dot === 'string') return dot;\n if (typeof dot === 'object' && dot !== null) {\n const conditions = dot as Record<string, unknown>;\n const target = conditions.default ?? conditions.import;\n if (typeof target === 'string') return target;\n }\n return undefined;\n}\n\nexport interface LoadedApp {\n definition: AppDefinition;\n manifest: AppManifest;\n entryPath: string;\n}\n\n/**\n * Imports an app's entry module and validates its default export.\n * The manifest is always re-validated here — the host never trusts\n * that an app used defineApp().\n */\nexport async function loadApp(repoDir: string): Promise<LoadedApp> {\n let pkg: PackageJson;\n try {\n pkg = JSON.parse(await readFile(join(repoDir, 'package.json'), 'utf8')) as PackageJson;\n } catch (err) {\n throw new AppSdkError(`App has no readable package.json in ${repoDir}`, { cause: err });\n }\n\n const entry = entryFromExports(pkg.exports) ?? pkg.main ?? 'index.js';\n const entryPath = join(repoDir, entry);\n\n let mod: Record<string, unknown>;\n try {\n // Cache-bust so `alashi update` reloads changed code within one process.\n mod = (await import(`${pathToFileURL(entryPath).href}?t=${Date.now()}`)) as Record<\n string,\n unknown\n >;\n } catch (err) {\n let detail = String(err);\n // defineApp() in the app's own bundled SDK validates at import time; an\n // \"unrecognized key\" there means the manifest uses fields newer than that\n // bundled SDK understands (cross-package, so match by name not instanceof).\n if ((err as Error)?.name === 'ManifestValidationError' && /Unrecognized key/i.test(detail)) {\n detail +=\n \"\\nThe manifest may use fields newer than the app's bundled @alashi/apps-sdk — update the app's dependency to match its sdkVersion.\";\n }\n throw new AppSdkError(`Failed to import app entry ${entry}: ${detail}`, { cause: err });\n }\n\n const definition = mod.default as AppDefinition | undefined;\n if (typeof definition !== 'object' || definition === null || !('manifest' in definition)) {\n throw new AppSdkError(\n 'App entry must default-export an app definition (use defineApp() from @alashi/apps-sdk)',\n );\n }\n\n // Check the declared SDK range before strict parsing: a manifest using\n // fields newer than this host's SDK should fail with \"update the host\",\n // not an unrecognized-key error.\n const raw = definition.manifest as { name?: unknown; sdkVersion?: unknown };\n if (typeof raw.sdkVersion === 'string') {\n checkSdkCompatibility({\n name: typeof raw.name === 'string' ? raw.name : entry,\n sdkVersion: raw.sdkVersion,\n });\n }\n\n const manifest = parseManifest(definition.manifest);\n return { definition, manifest, entryPath };\n}\n","import { execFile } from 'node:child_process';\nimport { mkdir, symlink } from 'node:fs/promises';\nimport { isAbsolute, resolve } from 'node:path';\nimport { promisify } from 'node:util';\nimport { AppSdkError } from '@alashi/apps-sdk';\n\nconst execFileAsync = promisify(execFile);\n\nexport type AppSourceSpec =\n | { type: 'git'; url: string; ref?: string }\n | { type: 'local'; path: string };\n\n/** How the CLI argument was understood: a source, or a name to resolve via registries. */\nexport type SourceArg = AppSourceSpec | { type: 'registry'; name: string };\n\nexport function parseSourceArg(arg: string): SourceArg {\n if (arg.startsWith('.') || isAbsolute(arg)) {\n return { type: 'local', path: resolve(arg) };\n }\n if (/^(https?:\\/\\/|git@|ssh:\\/\\/)/.test(arg)) {\n return { type: 'git', url: arg };\n }\n if (/^github\\.com\\//.test(arg)) {\n return { type: 'git', url: `https://${arg}` };\n }\n return { type: 'registry', name: arg };\n}\n\nexport async function git(args: string[], cwd?: string): Promise<string> {\n try {\n const { stdout } = await execFileAsync('git', args, cwd ? { cwd } : {});\n return stdout.trim();\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new AppSdkError(`git ${args[0]} failed: ${message}`, { cause: err });\n }\n}\n\n/** Materializes a source at destDir: git clone, or symlink for local dev installs. */\nexport async function fetchSource(spec: AppSourceSpec, destDir: string): Promise<void> {\n await mkdir(resolve(destDir, '..'), { recursive: true });\n if (spec.type === 'git') {\n const args = ['clone', '--depth', '1'];\n if (spec.ref) args.push('--branch', spec.ref);\n args.push(spec.url, destDir);\n await git(args);\n return;\n }\n await symlink(spec.path, destDir, 'dir');\n}\n\n/** Brings an installed source up to date in place. Local symlinks are always live. */\nexport async function updateSource(spec: AppSourceSpec, repoDir: string): Promise<void> {\n if (spec.type === 'git') {\n await git(['pull', '--ff-only'], repoDir);\n }\n}\n","import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { AppSdkError } from '@alashi/apps-sdk';\nimport { z } from 'zod';\nimport { fetchSource, git } from './sources.js';\nimport type { RegistryRef, Store } from './store.js';\n\nexport interface RegistryApp {\n name: string;\n repo: string;\n description?: string;\n ref?: string;\n sdkVersion?: string;\n}\n\nexport interface RegistryIndex {\n apps: RegistryApp[];\n}\n\nconst registryIndexSchema = z.object({\n apps: z.array(\n z.object({\n name: z.string().min(1),\n repo: z.string().min(1),\n description: z.string().optional(),\n ref: z.string().optional(),\n sdkVersion: z.string().optional(),\n }),\n ),\n});\n\nfunction parseIndex(raw: string, source: string): RegistryIndex {\n let data: unknown;\n try {\n data = JSON.parse(raw);\n } catch (err) {\n throw new AppSdkError(`Registry ${source} did not return valid JSON`, { cause: err });\n }\n const result = registryIndexSchema.safeParse(data);\n if (!result.success) {\n throw new AppSdkError(`Registry ${source} has an invalid index: ${result.error.message}`);\n }\n return result.data;\n}\n\n/**\n * Fetches a registry index and caches it under registries/<name>.json.\n * URLs ending in .json are fetched over HTTP; anything else is treated\n * as a git repo containing index.json at its root (brew-tap style).\n */\nexport async function refreshRegistry(store: Store, registry: RegistryRef): Promise<RegistryIndex> {\n let raw: string;\n if (/^https?:\\/\\/.*\\.json$/.test(registry.url)) {\n const response = await fetch(registry.url);\n if (!response.ok) {\n throw new AppSdkError(`Registry fetch failed (${response.status}) for ${registry.url}`);\n }\n raw = await response.text();\n } else {\n const cloneDir = join(store.stagingDir, `registry-${registry.name}-${Date.now()}`);\n await fetchSource({ type: 'git', url: registry.url }, cloneDir);\n try {\n raw = await readFile(join(cloneDir, 'index.json'), 'utf8');\n } finally {\n await rm(cloneDir, { recursive: true, force: true });\n }\n }\n const index = parseIndex(raw, registry.url);\n await mkdir(store.registriesDir, { recursive: true });\n await writeFile(store.registryCachePath(registry.name), JSON.stringify(index, null, 2), 'utf8');\n return index;\n}\n\nexport async function readCachedIndex(\n store: Store,\n name: string,\n): Promise<RegistryIndex | undefined> {\n try {\n return parseIndex(await readFile(store.registryCachePath(name), 'utf8'), name);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw err;\n }\n}\n\nexport async function addRegistry(store: Store, name: string, url: string): Promise<RegistryIndex> {\n const config = await store.readGlobalConfig();\n if (config.registries.some((r) => r.name === name)) {\n throw new AppSdkError(`Registry \"${name}\" already exists`);\n }\n const index = await refreshRegistry(store, { name, url });\n config.registries.push({ name, url });\n await store.writeGlobalConfig(config);\n return index;\n}\n\nexport async function removeRegistry(store: Store, name: string): Promise<void> {\n const config = await store.readGlobalConfig();\n const next = config.registries.filter((r) => r.name !== name);\n if (next.length === config.registries.length) {\n throw new AppSdkError(`Registry \"${name}\" is not configured`);\n }\n config.registries = next;\n await store.writeGlobalConfig(config);\n await rm(store.registryCachePath(name), { force: true });\n}\n\nexport interface ResolvedApp {\n registry: string;\n app: RegistryApp;\n}\n\n/** Finds an app by name across configured registries (refreshing caches). */\nexport async function resolveApp(store: Store, appName: string): Promise<ResolvedApp> {\n const config = await store.readGlobalConfig();\n if (config.registries.length === 0) {\n throw new AppSdkError(\n `No registries configured. Add one with: alashi registry add <name> <url>`,\n );\n }\n for (const registry of config.registries) {\n let index: RegistryIndex;\n try {\n index = await refreshRegistry(store, registry);\n } catch {\n const cached = await readCachedIndex(store, registry.name);\n if (!cached) continue;\n index = cached;\n }\n const app = index.apps.find((a) => a.name === appName);\n if (app) return { registry: registry.name, app };\n }\n throw new AppSdkError(`App \"${appName}\" not found in any configured registry`);\n}\n\nexport { git };\n","import { execFile } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport { mkdir, readFile, rename, rm } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { promisify } from 'node:util';\nimport { AppSdkError } from '@alashi/apps-sdk';\nimport { loadApp } from './loader.js';\nimport { resolveApp } from './registry.js';\nimport { type AppSourceSpec, fetchSource, parseSourceArg, updateSource } from './sources.js';\nimport type { InstallRecord, Store } from './store.js';\n\nconst execFileAsync = promisify(execFile);\n\nasync function hasDependencies(dir: string): Promise<boolean> {\n try {\n const pkg = JSON.parse(await readFile(join(dir, 'package.json'), 'utf8')) as {\n dependencies?: Record<string, string>;\n };\n return Object.keys(pkg.dependencies ?? {}).length > 0;\n } catch {\n return false;\n }\n}\n\n/** Lifecycle scripts stay off: installing an app must not execute arbitrary code. */\nexport async function installDependencies(dir: string): Promise<void> {\n if (!(await hasDependencies(dir))) return;\n try {\n await execFileAsync(\n 'npm',\n ['install', '--ignore-scripts', '--omit=dev', '--no-fund', '--no-audit'],\n { cwd: dir },\n );\n } catch (err) {\n throw new AppSdkError(`npm install failed: ${err instanceof Error ? err.message : err}`, {\n cause: err,\n });\n }\n}\n\nexport interface InstallOptions {\n /** Expected app name (enforced against the manifest when set). */\n expectName?: string;\n}\n\n/**\n * Install flow: materialize the source into a staging dir, install deps\n * (scripts disabled), import + validate the manifest, and only then move\n * it into apps/<name>/repo. A failed validation never leaves a broken app.\n */\nexport async function installApp(\n store: Store,\n source: string | AppSourceSpec,\n options: InstallOptions = {},\n): Promise<InstallRecord> {\n let spec: AppSourceSpec;\n let expectName = options.expectName;\n\n if (typeof source === 'string') {\n const parsed = parseSourceArg(source);\n if (parsed.type === 'registry') {\n const resolved = await resolveApp(store, parsed.name);\n spec = {\n type: 'git',\n url: resolved.app.repo,\n ...(resolved.app.ref && { ref: resolved.app.ref }),\n };\n expectName = expectName ?? parsed.name;\n } else {\n spec = parsed;\n }\n } else {\n spec = source;\n }\n\n // Local sources are symlinked in place; git sources are staged first.\n let stageDir: string | undefined;\n let inspectDir: string;\n if (spec.type === 'local') {\n inspectDir = spec.path;\n } else {\n stageDir = join(store.stagingDir, randomUUID());\n await fetchSource(spec, stageDir);\n await installDependencies(stageDir);\n inspectDir = stageDir;\n }\n\n try {\n const { manifest } = await loadApp(inspectDir);\n if (expectName && manifest.name !== expectName) {\n throw new AppSdkError(\n `App manifest name \"${manifest.name}\" does not match requested \"${expectName}\"`,\n );\n }\n const existing = await store.readAppRecord(manifest.name);\n if (existing) {\n throw new AppSdkError(\n `App \"${manifest.name}\" is already installed (use: alashi update ${manifest.name})`,\n );\n }\n\n const repoDir = store.appRepoDir(manifest.name);\n await mkdir(store.appDir(manifest.name), { recursive: true });\n if (stageDir) {\n await rename(stageDir, repoDir);\n stageDir = undefined;\n } else {\n await fetchSource(spec, repoDir);\n }\n await mkdir(store.appDataDir(manifest.name), { recursive: true });\n\n const now = new Date().toISOString();\n const record: InstallRecord = {\n name: manifest.name,\n version: manifest.version,\n ...(manifest.sdkVersion && { sdkVersion: manifest.sdkVersion }),\n source: spec,\n installedAt: now,\n updatedAt: now,\n };\n await store.writeAppRecord(record);\n return record;\n } finally {\n if (stageDir) await rm(stageDir, { recursive: true, force: true });\n }\n}\n\nexport async function updateApp(store: Store, name: string): Promise<InstallRecord> {\n const record = await store.readAppRecord(name);\n if (!record) throw new AppSdkError(`App \"${name}\" is not installed`);\n\n const repoDir = store.appRepoDir(name);\n await updateSource(record.source, repoDir);\n await installDependencies(repoDir);\n const { manifest } = await loadApp(repoDir);\n if (manifest.name !== name) {\n throw new AppSdkError(`App renamed itself to \"${manifest.name}\"; refusing update`);\n }\n\n const updated: InstallRecord = {\n ...record,\n version: manifest.version,\n ...(manifest.sdkVersion && { sdkVersion: manifest.sdkVersion }),\n updatedAt: new Date().toISOString(),\n };\n await store.writeAppRecord(updated);\n return updated;\n}\n\nexport interface RemoveOptions {\n /** Also delete the app's data/ and config (default keeps them). */\n purge?: boolean;\n}\n\nexport async function removeApp(\n store: Store,\n name: string,\n options: RemoveOptions = {},\n): Promise<void> {\n const record = await store.readAppRecord(name);\n if (!record) throw new AppSdkError(`App \"${name}\" is not installed`);\n\n if (options.purge) {\n await rm(store.appDir(name), { recursive: true, force: true });\n return;\n }\n await rm(store.appRepoDir(name), { recursive: true, force: true });\n await rm(store.appRecordPath(name), { force: true });\n}\n","import { execFile } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport {\n cp,\n lstat,\n mkdir,\n readdir,\n readFile,\n readlink,\n rename,\n rm,\n stat,\n writeFile,\n} from 'node:fs/promises';\nimport { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';\nimport { promisify } from 'node:util';\nimport {\n type AIProvider,\n APP_NAME_PATTERN,\n AppSdkError,\n type SessionTranscript,\n} from '@alashi/apps-sdk';\nimport { installDependencies } from './installer.js';\nimport type { InstallRecord, Store } from './store.js';\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * Container portability. An app is only half-portable on its own: the\n * container holds sessionId pointers, but the conversation transcripts live\n * with the engine (keyed by the app's data dir as cwd). An export therefore\n * snapshots the container *plus* the transcripts, engine-neutrally, and the\n * import re-keys them to this machine's data dir.\n *\n * Archive layout (tar.gz):\n * alashi-export.json { formatVersion, exportedAt, app, transcripts }\n * container/ the contents of $ALASHI_HOME/apps/<name>/\n * transcripts/<sessionId>.jsonl\n */\nexport const EXPORT_FORMAT_VERSION = 1;\n\nconst METADATA_FILE = 'alashi-export.json';\nconst CONTAINER_DIR = 'container';\nconst TRANSCRIPTS_DIR = 'transcripts';\nconst TRANSCRIPT_EXT = '.jsonl';\n\n/** Re-derivable from the source, and by far the biggest thing in a container. */\nconst CONTAINER_EXCLUDES = [join('repo', 'node_modules'), join('repo', '.git')];\n\n/** Session ids become file names, so they must not be able to escape the dir. */\nconst SESSION_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;\n\nexport interface ExportMetadata {\n formatVersion: number;\n exportedAt: string;\n app: InstallRecord;\n /** Session ids that have a transcripts/<id>.jsonl entry in the archive. */\n transcripts: string[];\n}\n\nexport interface ExportOptions {\n /** Archive path; defaults to ./<name>-<version>.tar.gz under the cwd. */\n out?: string;\n}\n\nexport interface ExportResult {\n archivePath: string;\n record: InstallRecord;\n transcriptCount: number;\n warnings: string[];\n}\n\nexport interface ImportResult {\n record: InstallRecord;\n transcriptCount: number;\n warnings: string[];\n}\n\n/** Shelling out to tar matches how git and npm are already invoked. */\nasync function tar(args: string[]): Promise<string> {\n try {\n const { stdout } = await execFileAsync('tar', args);\n return stdout;\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new AppSdkError(`tar ${args[0]} failed: ${message}`, { cause: err });\n }\n}\n\nasync function exists(path: string): Promise<boolean> {\n try {\n await lstat(path);\n return true;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return false;\n throw err;\n }\n}\n\nasync function isDirectory(path: string): Promise<boolean> {\n try {\n return (await stat(path)).isDirectory();\n } catch {\n return false;\n }\n}\n\nfunction containerFilter(appDir: string): (src: string) => boolean {\n return (src: string) => {\n const rel = relative(appDir, src);\n if (rel === '') return true;\n return !CONTAINER_EXCLUDES.some(\n (excluded) => rel === excluded || rel.startsWith(excluded + sep),\n );\n };\n}\n\n/**\n * Packs apps/<name>/ plus the transcripts of every conversation that ever had\n * a turn into a single self-contained tar.gz.\n */\nexport async function exportApp(\n store: Store,\n provider: AIProvider,\n name: string,\n options: ExportOptions = {},\n): Promise<ExportResult> {\n const record = await store.readAppRecord(name);\n if (!record) throw new AppSdkError(`App \"${name}\" is not installed`);\n\n const archivePath = resolve(\n process.cwd(),\n options.out ?? `${record.name}-${record.version}.tar.gz`,\n );\n if (await exists(archivePath)) {\n throw new AppSdkError(`Refusing to overwrite existing file: ${archivePath}`);\n }\n\n const warnings: string[] = [];\n const appDir = store.appDir(name);\n const stage = join(store.stagingDir, randomUUID());\n try {\n await mkdir(stage, { recursive: true });\n // dereference: local installs symlink repo/, and an archive must be\n // self-contained on the machine it lands on.\n await cp(appDir, join(stage, CONTAINER_DIR), {\n recursive: true,\n dereference: true,\n filter: containerFilter(appDir),\n });\n\n const { conversations } = await store.readChatState(name);\n const sessionIds = [\n ...new Set(\n conversations\n .map((conversation) => conversation.sessionId)\n .filter((sessionId): sessionId is string => !!sessionId),\n ),\n ];\n\n let transcripts: SessionTranscript[] = [];\n if (!provider.exportTranscripts) {\n if (sessionIds.length > 0) {\n warnings.push(\n `Provider \"${provider.name}\" cannot export transcripts; the archive holds the container only`,\n );\n }\n } else if (sessionIds.length > 0) {\n transcripts = await provider.exportTranscripts(sessionIds);\n }\n\n await mkdir(join(stage, TRANSCRIPTS_DIR), { recursive: true });\n const written: string[] = [];\n for (const transcript of transcripts) {\n if (!SESSION_ID_PATTERN.test(transcript.sessionId)) {\n warnings.push(\n `Skipped transcript with unusable session id ${JSON.stringify(transcript.sessionId)}`,\n );\n continue;\n }\n await writeFile(\n join(stage, TRANSCRIPTS_DIR, `${transcript.sessionId}${TRANSCRIPT_EXT}`),\n transcript.content,\n 'utf8',\n );\n written.push(transcript.sessionId);\n }\n if (provider.exportTranscripts && written.length < sessionIds.length) {\n warnings.push(\n `${sessionIds.length - written.length} of ${sessionIds.length} conversations had no transcript in \"${provider.name}\" storage`,\n );\n }\n\n const metadata: ExportMetadata = {\n formatVersion: EXPORT_FORMAT_VERSION,\n exportedAt: new Date().toISOString(),\n app: record,\n transcripts: written,\n };\n await writeFile(join(stage, METADATA_FILE), `${JSON.stringify(metadata, null, 2)}\\n`, 'utf8');\n\n await mkdir(dirname(archivePath), { recursive: true });\n await tar(['-czf', archivePath, '-C', stage, '.']);\n\n return { archivePath, record, transcriptCount: written.length, warnings };\n } finally {\n await rm(stage, { recursive: true, force: true });\n }\n}\n\n/** An archive is untrusted input: no absolute members, no \"..\" traversal. */\nasync function assertSafeMembers(archive: string): Promise<void> {\n const listing = await tar(['-tzf', archive]);\n for (const line of listing.split('\\n')) {\n const member = line.trim();\n if (member === '') continue;\n const segments = member.split(/[\\\\/]/);\n if (isAbsolute(member) || member.startsWith('/') || segments.includes('..')) {\n throw new AppSdkError(\n `Refusing to import ${archive}: it contains an unsafe path ${JSON.stringify(member)}`,\n );\n }\n }\n}\n\n/** Second belt: a symlink inside the archive must not point out of the stage. */\nasync function assertNothingEscaped(stage: string): Promise<void> {\n const root = resolve(stage);\n const walk = async (dir: string): Promise<void> => {\n for (const entry of await readdir(dir, { withFileTypes: true })) {\n const path = join(dir, entry.name);\n if (entry.isSymbolicLink()) {\n const target = resolve(dir, await readlink(path));\n const rel = relative(root, target);\n if (rel === '' || rel.startsWith('..') || isAbsolute(rel)) {\n throw new AppSdkError(\n `Refusing to import: ${relative(root, path)} links outside the archive (${target})`,\n );\n }\n continue;\n }\n if (entry.isDirectory()) await walk(path);\n }\n };\n await walk(root);\n}\n\nasync function readMetadata(path: string): Promise<ExportMetadata> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf8');\n } catch (err) {\n throw new AppSdkError(`Not an alashi export archive: ${METADATA_FILE} is missing`, {\n cause: err,\n });\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch (err) {\n throw new AppSdkError(`Corrupt ${METADATA_FILE} in archive`, { cause: err });\n }\n const metadata = parsed as Partial<ExportMetadata> | null;\n if (metadata?.formatVersion !== EXPORT_FORMAT_VERSION) {\n throw new AppSdkError(\n `Unsupported export formatVersion ${JSON.stringify(metadata?.formatVersion)}; this alashi reads formatVersion ${EXPORT_FORMAT_VERSION}`,\n );\n }\n return metadata as ExportMetadata;\n}\n\nasync function readInstallRecord(path: string): Promise<InstallRecord> {\n let parsed: unknown;\n try {\n parsed = JSON.parse(await readFile(path, 'utf8'));\n } catch (err) {\n throw new AppSdkError(`Archive has no readable ${CONTAINER_DIR}/app.json`, { cause: err });\n }\n const record = parsed as Partial<InstallRecord> | null;\n if (!record || typeof record.name !== 'string' || typeof record.version !== 'string') {\n throw new AppSdkError(`Archive ${CONTAINER_DIR}/app.json is not an install record`);\n }\n if (!APP_NAME_PATTERN.test(record.name)) {\n throw new AppSdkError(`Archive carries an invalid app name: ${JSON.stringify(record.name)}`);\n }\n return record as InstallRecord;\n}\n\nasync function readTranscripts(dir: string): Promise<SessionTranscript[]> {\n let entries: string[];\n try {\n entries = await readdir(dir);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];\n throw err;\n }\n const transcripts: SessionTranscript[] = [];\n for (const entry of entries.sort()) {\n if (!entry.endsWith(TRANSCRIPT_EXT)) continue;\n const sessionId = entry.slice(0, -TRANSCRIPT_EXT.length);\n if (!SESSION_ID_PATTERN.test(sessionId)) continue;\n transcripts.push({ sessionId, content: await readFile(join(dir, entry), 'utf8') });\n }\n return transcripts;\n}\n\n/** Staging and apps/ are both under $ALASHI_HOME, so rename is the fast path. */\nasync function moveDir(from: string, to: string): Promise<void> {\n try {\n await rename(from, to);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EXDEV') throw err;\n await cp(from, to, { recursive: true });\n await rm(from, { recursive: true, force: true });\n }\n}\n\n/** Restores a container exported by {@link exportApp}, plus its transcripts. */\nexport async function importApp(\n store: Store,\n provider: AIProvider,\n archivePath: string,\n): Promise<ImportResult> {\n const archive = resolve(process.cwd(), archivePath);\n if (!(await exists(archive))) throw new AppSdkError(`Archive not found: ${archive}`);\n await assertSafeMembers(archive);\n\n const warnings: string[] = [];\n const stage = join(store.stagingDir, randomUUID());\n try {\n await mkdir(stage, { recursive: true });\n await tar(['-xzf', archive, '-C', stage, '--no-same-owner']);\n await assertNothingEscaped(stage);\n\n await readMetadata(join(stage, METADATA_FILE));\n const containerDir = join(stage, CONTAINER_DIR);\n if (!(await isDirectory(containerDir))) {\n throw new AppSdkError(`Archive has no ${CONTAINER_DIR}/ directory: ${archive}`);\n }\n\n const record = await readInstallRecord(join(containerDir, 'app.json'));\n const name = record.name;\n if (await store.readAppRecord(name)) {\n throw new AppSdkError(`App \"${name}\" is already installed (use: alashi remove ${name})`);\n }\n const appDir = store.appDir(name);\n if (await exists(appDir)) {\n throw new AppSdkError(\n `${appDir} already exists (leftover data from a previous install); delete it before importing`,\n );\n }\n\n await mkdir(store.appsDir, { recursive: true });\n await moveDir(containerDir, appDir);\n\n // Past this point the container is live: every failure has to say so\n // rather than look like a clean abort.\n try {\n await mkdir(store.appDataDir(name), { recursive: true });\n await installDependencies(store.appRepoDir(name));\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new AppSdkError(\n `Restored ${name} to ${appDir}, but reinstalling its dependencies failed: ${message}. Run \"alashi update ${name}\" before using it.`,\n { cause: err },\n );\n }\n\n const transcripts = await readTranscripts(join(stage, TRANSCRIPTS_DIR));\n let restored = 0;\n if (transcripts.length > 0) {\n if (!provider.importTranscripts) {\n warnings.push(\n `Provider \"${provider.name}\" cannot import transcripts; ${transcripts.length} conversation transcripts were not restored`,\n );\n } else {\n try {\n // The *destination* data dir: that is what re-keys the transcripts\n // for this machine's engine storage.\n await provider.importTranscripts(transcripts, { cwd: store.appDataDir(name) });\n } catch (err) {\n const message = err instanceof Error ? err.message : String(err);\n throw new AppSdkError(\n `Restored ${name} to ${appDir}, but restoring its transcripts failed: ${message}. The app works; its chat history does not.`,\n { cause: err },\n );\n }\n restored = transcripts.length;\n }\n }\n\n return { record, transcriptCount: restored, warnings };\n } finally {\n await rm(stage, { recursive: true, force: true });\n }\n}\n","import { mkdir, readFile } from 'node:fs/promises';\nimport { basename, join, resolve } from 'node:path';\nimport {\n type AgentDefinition,\n type AIProvider,\n type AISession,\n type AppConfigStatus,\n type AppContext,\n type AppManifest,\n AppSdkError,\n ConfigError,\n type ConfigObject,\n evaluateAppConfig,\n interpolateConfig,\n JsonFileStore,\n type Logger,\n type McpServerConfig,\n manifestConfigDefaults,\n mergeConfig,\n parseAgentFile,\n type SessionOptions,\n} from '@alashi/apps-sdk';\nimport { assertWithinBudget } from './budget.js';\nimport type { Store } from './store.js';\nimport type { UsageRecorder } from './usage.js';\n\nfunction createLogger(prefix: string): Logger {\n const log = (\n level: 'debug' | 'info' | 'warn' | 'error',\n message: string,\n data?: Record<string, unknown>,\n ): void => {\n const line = `[${prefix}] ${message}`;\n if (data === undefined) console[level](line);\n else console[level](line, data);\n };\n return {\n debug: (message, data) => log('debug', message, data),\n info: (message, data) => log('info', message, data),\n warn: (message, data) => log('warn', message, data),\n error: (message, data) => log('error', message, data),\n };\n}\n\ninterface SessionDefaults {\n mcpServers: Record<string, McpServerConfig>;\n allowedTools: string[];\n agents: Record<string, AgentDefinition>;\n skillDirs: string[];\n instructions?: string;\n cwd: string;\n /** When set, createSession fails closed with this message (missing required config). */\n unreadyError?: string;\n /**\n * Second fail-closed gate: throws `BudgetExceededError` when the app has\n * already spent its host-set daily cap. Called on every createSession, so\n * the answer reflects spend and cap as of right now.\n */\n checkBudget?: () => Promise<void>;\n /**\n * Called once per `result` event with the cost the engine reported (possibly\n * undefined). Must not throw — the caller's stream is not the place to fail.\n */\n recordCost?: (costUsd: number | undefined) => Promise<void>;\n}\n\n/**\n * Wraps a session so every `result` event's cost is accounted for before the\n * event reaches the caller. This sits under *every* session an app opens —\n * chat turns, cron job handlers, and app routes alike — so spend is recorded\n * no matter who opened the session.\n */\nfunction meterSession(\n session: AISession,\n recordCost: (costUsd: number | undefined) => Promise<void>,\n): AISession {\n return {\n get id() {\n return session.id;\n },\n send(input: string) {\n const events = session.send(input);\n return (async function* metered() {\n for await (const event of events) {\n if (event.type === 'result') await recordCost(event.costUsd);\n yield event;\n }\n })();\n },\n interrupt: () => session.interrupt(),\n close: () => session.close(),\n };\n}\n\n/**\n * Wraps the host provider so every session an app opens automatically gets\n * its manifest-declared MCP servers, tool allowlist, agents, skills,\n * instructions, and data-dir cwd. Per-call options still win where they\n * overlap.\n */\nfunction scopeProvider(provider: AIProvider, defaults: SessionDefaults): AIProvider {\n return {\n name: provider.name,\n async createSession(options: SessionOptions = {}): Promise<AISession> {\n if (defaults.unreadyError !== undefined) throw new ConfigError(defaults.unreadyError);\n // Money gate, above provider.createSession on purpose: the engine process\n // is the first thing that can cost, so an over-budget app must be turned\n // away before one is ever spawned.\n await defaults.checkBudget?.();\n const mcpServers = { ...defaults.mcpServers, ...options.mcpServers };\n const allowedTools = [\n ...new Set([...defaults.allowedTools, ...(options.allowedTools ?? [])]),\n ];\n const agents = { ...defaults.agents, ...options.agents };\n const skillDirs = [...new Set([...defaults.skillDirs, ...(options.skillDirs ?? [])])];\n const session = await provider.createSession({\n cwd: defaults.cwd,\n ...(defaults.instructions !== undefined && { systemPrompt: defaults.instructions }),\n ...options,\n ...(Object.keys(mcpServers).length > 0 && { mcpServers }),\n ...(allowedTools.length > 0 && { allowedTools }),\n ...(Object.keys(agents).length > 0 && { agents }),\n ...(skillDirs.length > 0 && { skillDirs }),\n });\n return defaults.recordCost ? meterSession(session, defaults.recordCost) : session;\n },\n };\n}\n\n/**\n * Reads and parses the manifest-declared agent files. The agent's name is\n * the filename without the `.md` extension.\n */\nexport async function loadAgents(\n repoDir: string,\n paths: string[],\n): Promise<Record<string, AgentDefinition>> {\n const entries = await Promise.all(\n paths.map(async (path) => {\n const content = await readFile(resolve(repoDir, path), 'utf8');\n return [basename(path, '.md'), parseAgentFile(content, path)] as const;\n }),\n );\n return Object.fromEntries(entries);\n}\n\n/** Reads the manifest-declared instructions file; the content is the app's system prompt. */\nexport async function loadInstructions(\n repoDir: string,\n path: string | undefined,\n): Promise<string | undefined> {\n if (path === undefined) return undefined;\n const content = (await readFile(resolve(repoDir, path), 'utf8')).trim();\n if (content === '') {\n throw new AppSdkError(`Instructions file \"${path}\" is empty`);\n }\n return content;\n}\n\nexport interface ResolvedAppConfig {\n /** Merged config layers, before `${config.*}` interpolation. */\n merged: ConfigObject;\n status: AppConfigStatus;\n}\n\n/** Merges the config layers for an app and evaluates its setup state. */\nexport async function resolveAppConfig(\n store: Store,\n manifest: AppManifest,\n): Promise<ResolvedAppConfig> {\n const globalConfig = await store.readGlobalConfig();\n const merged = mergeConfig(\n manifestConfigDefaults(manifest),\n globalConfig.apps?.[manifest.name],\n await store.readAppConfig(manifest.name),\n );\n return { merged, status: evaluateAppConfig(manifest, merged) };\n}\n\n/**\n * Builds the AppContext for an installed app: merges the config layers\n * (manifest defaults < global apps[name] < per-app config), resolves\n * `${config.*}` placeholders (with `dataDir` available as a built-in key),\n * and scopes the AI provider to the app's manifest.\n *\n * Setup state shapes the session defaults: unconfigured MCP servers are\n * dropped (with their tools) so interpolation never hits a missing key, and\n * while required keys are missing the scoped provider fails closed.\n *\n * When a `usage` recorder is supplied, every session opened through the\n * returned context reports its per-turn cost to it, and the app's host-set\n * daily spend cap (if any) is enforced on every createSession.\n */\nexport async function buildAppContext(\n store: Store,\n manifest: AppManifest,\n provider: AIProvider,\n usage?: UsageRecorder,\n): Promise<AppContext> {\n const name = manifest.name;\n const dataDir = store.appDataDir(name);\n const repoDir = store.appRepoDir(name);\n await mkdir(dataDir, { recursive: true });\n\n const { merged, status } = await resolveAppConfig(store, manifest);\n // dataDir is a host-provided lookup key: apps reference it as ${config.dataDir}.\n const lookup: ConfigObject = { ...merged, dataDir };\n const interpolate = <T>(value: T): T =>\n interpolateConfig(value, lookup, { resolveRelativeTo: repoDir });\n\n const config = interpolate(merged);\n\n const instructions = await loadInstructions(repoDir, manifest.instructions);\n\n const excluded = status.integrations.filter((i) => !i.configured).map((i) => i.name);\n const activeServers = Object.fromEntries(\n Object.entries(manifest.mcpServers)\n .filter(([server]) => !excluded.includes(server))\n .map(([server, { optional: _optional, ...decl }]) => [server, decl as McpServerConfig]),\n );\n const mcpServers = interpolate(activeServers);\n const allowedTools = manifest.permissions.allowedTools.filter(\n (tool) => !excluded.some((server) => tool.startsWith(`mcp__${server}__`)),\n );\n\n const logger = createLogger(name);\n // Bookkeeping must never break a turn: a failed usage write is logged, not thrown.\n const recordCost = usage\n ? async (costUsd: number | undefined): Promise<void> => {\n try {\n await usage.record(name, costUsd);\n } catch (err) {\n logger.warn('Failed to record AI spend', { error: String(err) });\n }\n }\n : undefined;\n\n return {\n appName: name,\n dataDir,\n config,\n logger,\n state: new JsonFileStore(join(dataDir, 'state.json')),\n ai: scopeProvider(provider, {\n mcpServers,\n allowedTools,\n agents: await loadAgents(repoDir, manifest.agents),\n skillDirs: manifest.skills.map((dir) => resolve(repoDir, dir)),\n ...(instructions !== undefined && { instructions }),\n cwd: dataDir,\n ...(recordCost !== undefined && { recordCost }),\n // Enforcement rides with accounting: without a recorder nothing is\n // metered, so there is no spend to compare a cap against.\n ...(usage !== undefined && {\n checkBudget: (): Promise<void> => assertWithinBudget(store, usage, name),\n }),\n ...(!status.ready && {\n unreadyError: `App \"${name}\" is not configured: missing ${status.missing.join(', ')}. Set the missing keys in the dashboard settings or ${store.appConfigPath(name)}.`,\n }),\n }),\n };\n}\n","import type { AppContext, AppManifest } from '@alashi/apps-sdk';\nimport { Cron } from 'croner';\nimport type { JobRun, Store } from './store.js';\n\nexport interface SchedulableApp {\n manifest: AppManifest;\n ctx: AppContext;\n jobHandlers?: Record<string, (ctx: AppContext) => Promise<void>>;\n}\n\nexport interface JobStatus {\n name: string;\n schedule: string;\n description?: string;\n /** False when the cron expression failed to parse (job never fires). */\n valid: boolean;\n running: boolean;\n nextRun?: string;\n lastRun?: string;\n recentRuns: JobRun[];\n}\n\nconst RUN_LOG_CAP = 50;\nconst RECENT_RUNS = 5;\n\nfunction nextFire(schedule: string, after?: Date): Date | null {\n // A Cron without a callback only computes times; it never schedules timers.\n return new Cron(schedule).nextRun(after ?? undefined);\n}\n\n/**\n * Runs manifest-declared jobs on their cron schedules. One instance per host\n * process; start() schedules every installed app's jobs, stop() cancels them.\n * A job missed while the host was down runs once at startup (\"catchup\"), but\n * only if it has run before — a fresh install waits for its first slot.\n */\nexport class Scheduler {\n private crons: Cron[] = [];\n private runningJobs = new Set<string>();\n private started = false;\n /** Serializes jobs.json read-modify-write across concurrently finishing jobs. */\n private stateQueue: Promise<unknown> = Promise.resolve();\n\n constructor(\n private readonly store: Store,\n private readonly getApp: (name: string) => Promise<SchedulableApp | undefined>,\n ) {}\n\n async start(): Promise<void> {\n if (this.started) return;\n this.started = true;\n for (const record of await this.store.listInstalled()) {\n let app: SchedulableApp | undefined;\n try {\n app = await this.getApp(record.name);\n } catch (err) {\n console.error(`[scheduler] skipping ${record.name}: failed to load (${String(err)})`);\n continue;\n }\n if (!app || app.manifest.jobs.length === 0) continue;\n const state = await this.store.readJobState(record.name);\n for (const job of app.manifest.jobs) {\n let cron: Cron;\n try {\n cron = new Cron(job.schedule, { paused: true }, () => {\n void this.run(record.name, job.name, 'schedule');\n });\n } catch (err) {\n app.ctx.logger.error(`Job \"${job.name}\" has an invalid schedule`, {\n schedule: job.schedule,\n error: String(err),\n });\n continue;\n }\n const last = state.lastRun[job.name];\n if (last) {\n const missed = nextFire(job.schedule, new Date(last));\n if (missed && missed.getTime() <= Date.now()) {\n void this.run(record.name, job.name, 'catchup');\n }\n }\n cron.resume();\n this.crons.push(cron);\n }\n }\n }\n\n stop(): void {\n for (const cron of this.crons) cron.stop();\n this.crons = [];\n this.started = false;\n }\n\n /**\n * Runs one job immediately. Returns the recorded run, 'skipped' when that\n * job is already mid-run, or undefined when the app/job doesn't exist.\n */\n async run(\n appName: string,\n jobName: string,\n trigger: JobRun['trigger'] = 'manual',\n ): Promise<JobRun | 'skipped' | undefined> {\n const app = await this.getApp(appName).catch(() => undefined);\n const handler = app?.jobHandlers?.[jobName];\n if (!app || !handler || !app.manifest.jobs.some((j) => j.name === jobName)) return undefined;\n\n const key = `${appName}/${jobName}`;\n if (this.runningJobs.has(key)) return 'skipped';\n this.runningJobs.add(key);\n\n const startedAt = new Date().toISOString();\n let ok = true;\n let error: string | undefined;\n try {\n await handler(app.ctx);\n } catch (err) {\n ok = false;\n error = String(err);\n app.ctx.logger.error(`Job \"${jobName}\" failed`, { error });\n } finally {\n this.runningJobs.delete(key);\n }\n\n const run: JobRun = {\n job: jobName,\n trigger,\n startedAt,\n finishedAt: new Date().toISOString(),\n ok,\n ...(error !== undefined && { error }),\n };\n await this.recordRun(appName, run);\n return run;\n }\n\n /** Job statuses for an app, or undefined when the app isn't installed. */\n async status(appName: string): Promise<JobStatus[] | undefined> {\n const app = await this.getApp(appName).catch(() => undefined);\n if (!app) return undefined;\n const state = await this.store.readJobState(appName);\n return app.manifest.jobs.map((job) => {\n let valid = true;\n let nextRun: string | undefined;\n try {\n nextRun = nextFire(job.schedule)?.toISOString();\n } catch {\n valid = false;\n }\n return {\n name: job.name,\n schedule: job.schedule,\n ...(job.description !== undefined && { description: job.description }),\n valid,\n running: this.runningJobs.has(`${appName}/${job.name}`),\n ...(nextRun !== undefined && { nextRun }),\n ...(state.lastRun[job.name] !== undefined && { lastRun: state.lastRun[job.name] }),\n recentRuns: state.runs\n .filter((r) => r.job === job.name)\n .slice(-RECENT_RUNS)\n .reverse(),\n };\n });\n }\n\n private recordRun(appName: string, run: JobRun): Promise<void> {\n const update = this.stateQueue.then(async () => {\n const state = await this.store.readJobState(appName);\n state.lastRun[run.job] = run.startedAt;\n state.runs.push(run);\n if (state.runs.length > RUN_LOG_CAP) state.runs.splice(0, state.runs.length - RUN_LOG_CAP);\n await this.store.writeJobState(appName, state);\n });\n this.stateQueue = update.catch(() => undefined);\n return update;\n }\n}\n","/**\n * Self-contained per-app settings page. Config data comes from\n * GET /api/apps/:name/config at runtime; saving PUTs only changed keys.\n * The read-only \"App definition\" section (instructions, agents, skills,\n * allowed tools, MCP servers, jobs) comes from GET /api/apps/:name/manifest.\n * The app name is injected via a JSON-encoded script constant, never into\n * markup, so no escaping subtleties apply.\n */\nexport function settingsPage(name: string): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>Settings</title>\n<link rel=\"stylesheet\" href=\"/assets/v1/system.css\">\n<style>\n .card { margin-bottom: 16px; }\n #status { margin-left: 12px; color: var(--muted); }\n #manifest-cards { margin-top: 32px; }\n .ro-pre { white-space: pre-wrap; word-break: break-word; margin: 8px 0 0; }\n .ro-item { margin-bottom: 12px; }\n .ro-item:last-child { margin-bottom: 0; }\n details summary { cursor: pointer; }\n #budget-card input { max-width: 180px; }\n #budget-status { margin-left: 12px; color: var(--muted); }\n</style>\n</head>\n<body>\n<header class=\"topbar\"><a href=\"/\">alashi</a> <span>/</span> <strong id=\"title\"></strong> <span>settings</span></header>\n<main class=\"narrow\">\n <div id=\"banner\" class=\"banner\" hidden></div>\n <div id=\"budget-card\"></div>\n <div id=\"cards\"></div>\n <button id=\"save\" class=\"primary\">Save changes</button><span id=\"status\"></span>\n <div id=\"manifest-cards\"></div>\n</main>\n<script>\nconst APP = ${JSON.stringify(name)};\ndocument.getElementById('title').textContent = APP;\n\nconst state = { data: null, dirty: new Map() };\n\nfunction el(tag, attrs = {}, children = []) {\n const node = document.createElement(tag);\n for (const [k, v] of Object.entries(attrs)) {\n if (k === 'text') node.textContent = v;\n else if (k.startsWith('on')) node.addEventListener(k.slice(2), v);\n else node.setAttribute(k, v);\n }\n for (const child of children) node.append(child);\n return node;\n}\n\nfunction parseInput(raw) {\n if (raw === '') return null; // empty clears the user-set value\n try { const v = JSON.parse(raw); if (typeof v !== 'string') return v; } catch {}\n return raw;\n}\n\nfunction displayValue(value) {\n return typeof value === 'string' ? value : value === undefined ? '' : JSON.stringify(value);\n}\n\nconst ACRONYMS = new Set(['url', 'uri', 'api', 'id', 'ip', 'db', 'ssl', 'tls', 'ttl', 'http', 'https']);\n\nfunction labelFor(key, decl) {\n if (decl.label) return decl.label;\n const words = key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[_-]+/g, ' ')\n .toLowerCase()\n .split(' ');\n return words\n .map((w, i) => (ACRONYMS.has(w) ? w.toUpperCase() : i === 0 ? w.charAt(0).toUpperCase() + w.slice(1) : w))\n .join(' ');\n}\n\nfunction field(key, decl, data) {\n const secret = decl.secret === true;\n const isBool = !secret &&\n (typeof decl.default === 'boolean' || typeof data.values[key] === 'boolean');\n if (isBool) {\n const input = el('input', {\n type: 'checkbox',\n onchange: () => state.dirty.set(key, { secret: false, input }),\n });\n input.checked = data.values[key] === true;\n const labelChildren = [input, document.createTextNode(labelFor(key, decl))];\n if (decl.required) labelChildren.push(el('span', { class: 'req', text: ' *' }));\n const parts = [el('label', { class: 'check' }, labelChildren)];\n if (decl.description) parts.push(el('div', { class: 'desc', text: decl.description }));\n return el('div', { class: 'field' }, parts);\n }\n const isSet = secret ? data.secretsSet.includes(key) : data.values[key] !== undefined;\n const input = el('input', {\n type: secret ? 'password' : 'text',\n value: secret ? '' : displayValue(data.values[key]),\n placeholder: secret && isSet ? '(set — type to replace)' : '',\n oninput: () => state.dirty.set(key, { secret, input }),\n });\n const labelChildren = [document.createTextNode(labelFor(key, decl))];\n if (decl.required) labelChildren.push(el('span', { class: 'req', text: ' *' }));\n const parts = [el('label', {}, labelChildren)];\n if (decl.description) parts.push(el('div', { class: 'desc', text: decl.description }));\n parts.push(input);\n return el('div', { class: 'field' }, parts);\n}\n\nfunction card(title, keys, data, configured) {\n const chip = configured === undefined\n ? ''\n : el('span', { class: 'chip ' + (configured ? 'on' : 'off'),\n text: configured ? 'configured' : 'not configured' });\n return el('div', { class: 'card' }, [\n el('h2', {}, chip === '' ? [document.createTextNode(title)] : [document.createTextNode(title), chip]),\n el('div', { class: 'body' }, keys.map((key) => field(key, data.schema[key] ?? {}, data))),\n ]);\n}\n\nfunction render(data) {\n state.data = data;\n state.dirty.clear();\n const banner = document.getElementById('banner');\n banner.hidden = false;\n if (data.ready) {\n banner.className = 'banner ok';\n banner.textContent = 'App is configured and ready.';\n } else {\n banner.className = 'banner warn';\n banner.textContent = 'Needs setup — missing: ' + data.missing.join(', ');\n }\n\n const cards = document.getElementById('cards');\n cards.replaceChildren();\n const integrationKeys = new Set(data.integrations.flatMap((i) => i.configKeys));\n\n // Non-integration keys, sectioned by their declared group (\"General\" first).\n const sections = new Map();\n for (const key of Object.keys(data.schema)) {\n if (integrationKeys.has(key)) continue;\n const title = data.schema[key].group ?? 'General';\n if (!sections.has(title)) sections.set(title, []);\n sections.get(title).push(key);\n }\n if (sections.has('General')) cards.append(card('General', sections.get('General'), data, undefined));\n for (const [title, keys] of sections) {\n if (title !== 'General') cards.append(card(title, keys, data, undefined));\n }\n\n // Integration cards, titled by the keys' shared group when declared.\n for (const integration of data.integrations) {\n if (integration.configKeys.length === 0) continue;\n const groups = new Set(integration.configKeys.map((k) => data.schema[k]?.group).filter(Boolean));\n const title = groups.size === 1 ? [...groups][0] : integration.name;\n cards.append(card(\n title + (integration.optional ? ' (optional integration)' : ''),\n integration.configKeys, data, integration.configured,\n ));\n }\n if (cards.childElementCount === 0) {\n cards.append(el('p', { text: 'This app declares no configurable settings.' }));\n }\n}\n\nfunction usd(value) {\n return '$' + value.toFixed(value >= 1 ? 2 : 4);\n}\n\n/**\n * Daily spend cap. A host setting, not an app config key: it lives in the\n * host's own config.json and is enforced in createSession, so an app cannot\n * raise its own ceiling through its manifest defaults or its config layer.\n */\nfunction renderBudget(b, message) {\n const root = document.getElementById('budget-card');\n root.replaceChildren();\n const capped = typeof b.dailyUsd === 'number';\n\n const input = el('input', {\n type: 'number', min: '0', step: '0.01', placeholder: 'no cap',\n value: capped ? String(b.dailyUsd) : '',\n });\n const status = el('span', { id: 'budget-status', text: message || '' });\n\n const save = el('button', {\n class: 'primary',\n text: 'Save cap',\n onclick: async () => {\n const raw = input.value.trim();\n const dailyUsd = raw === '' ? null : Number(raw);\n if (dailyUsd !== null && !(dailyUsd >= 0)) {\n status.textContent = 'Enter a dollar amount, or leave empty for no cap.';\n return;\n }\n status.textContent = 'Saving…';\n const res = await fetch('/api/apps/' + APP + '/budget', {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify({ dailyUsd }),\n });\n if (!res.ok) {\n status.textContent = 'Save failed (' + res.status + ')';\n return;\n }\n renderBudget(await res.json(), dailyUsd === null ? 'Cap removed.' : 'Cap saved.');\n },\n });\n\n const spent = 'Spent today (' + b.date + ', UTC): ' + usd(b.spentUsd);\n const body = [\n el('div', { class: 'desc', text:\n 'Enforced by the host: once the spend for the UTC day reaches the cap, new AI sessions for '\n + 'this app are refused, and the counter resets at 00:00 UTC. This is a host setting — the '\n + 'app cannot read or raise it through its own config.' }),\n el('div', { class: 'field' }, [\n el('label', { text: 'Daily spend cap (USD)' }),\n input,\n ]),\n el('div', { class: 'desc', text: capped\n ? spent + ' — ' + usd(b.remainingUsd) + ' left of ' + usd(b.dailyUsd)\n : spent + ' — no cap set, spend is unlimited' }),\n el('div', { style: 'margin-top:12px' }, [save, status]),\n ];\n if (b.overBudget) {\n body.unshift(el('div', { class: 'banner warn', text:\n 'Over budget — new AI sessions are refused. ' + usd(b.spentUsd) + ' spent of the '\n + usd(b.dailyUsd) + ' daily cap for ' + b.date + ' (UTC). Raise or clear the cap below, '\n + 'or wait for the reset at 00:00 UTC.' }));\n }\n\n root.append(el('div', { class: 'card' }, [\n el('h2', { text: 'Spend cap' }),\n el('div', { class: 'body' }, body),\n ]));\n}\n\nfunction roCard(title, children) {\n return el('div', { class: 'card' }, [\n el('h2', { text: title }),\n el('div', { class: 'body' }, children),\n ]);\n}\n\nfunction renderManifest(m) {\n const root = document.getElementById('manifest-cards');\n root.replaceChildren();\n const cards = [];\n\n const about = [el('div', { class: 'desc', text: 'Version ' + m.version + (m.model ? ' · model: ' + m.model : '') })];\n if (m.description) about.unshift(el('div', { text: m.description }));\n cards.push(roCard('About', about));\n\n if (m.instructions) {\n cards.push(roCard('Instructions', [el('pre', { class: 'ro-pre', text: m.instructions })]));\n }\n\n const agentNames = Object.keys(m.agents);\n if (agentNames.length > 0) {\n cards.push(roCard('Agents', agentNames.map((name) => {\n const a = m.agents[name];\n const meta = [a.model && 'model: ' + a.model, a.tools && 'tools: ' + a.tools.join(', ')]\n .filter(Boolean).join(' · ');\n const body = meta ? [el('div', { class: 'desc', text: meta })] : [];\n body.push(el('pre', { class: 'ro-pre', text: a.prompt }));\n return el('details', { class: 'ro-item' }, [\n el('summary', {}, [el('strong', { text: name }), document.createTextNode(' — ' + a.description)]),\n ...body,\n ]);\n })));\n }\n\n if (m.skills.length > 0) {\n cards.push(roCard('Skills', m.skills.map((s) => el('div', { class: 'ro-item' }, [\n el('code', { text: s.dir + '/' }),\n el('div', { class: 'desc', text: s.entries.length ? s.entries.join(', ') : '(directory not found or empty)' }),\n ]))));\n }\n\n if (m.mcpServers.length > 0) {\n cards.push(roCard('MCP servers', m.mcpServers.map((s) => el('div', { class: 'ro-item' }, [\n el('strong', { text: s.name }),\n document.createTextNode(' (' + s.type + (s.optional ? ', optional' : '') + ') '),\n el('span', { class: 'chip ' + (s.configured ? 'on' : 'off'),\n text: s.configured ? 'configured' : 'not configured' }),\n el('div', { class: 'desc', text: s.type === 'stdio' ? s.command : s.url }),\n ]))));\n }\n\n if (m.allowedTools.length > 0) {\n cards.push(roCard('Allowed tools', m.allowedTools.map((t) =>\n el('div', {}, [el('code', { text: t })]))));\n }\n\n if (m.jobs.length > 0) {\n cards.push(roCard('Jobs', m.jobs.map((j) => el('div', { class: 'ro-item' }, [\n el('strong', { text: j.name }),\n el('code', { text: ' ' + j.schedule }),\n ...(j.description ? [el('div', { class: 'desc', text: j.description })] : []),\n ]))));\n }\n\n root.append(\n el('h1', { text: 'App definition' }),\n el('p', { class: 'subtitle', text: 'Declared by the app; read-only.' }),\n ...cards,\n );\n}\n\nasync function load() {\n const [res, manRes, budgetRes] = await Promise.all([\n fetch('/api/apps/' + APP + '/config'),\n fetch('/api/apps/' + APP + '/manifest'),\n fetch('/api/apps/' + APP + '/budget'),\n ]);\n // Host bookkeeping: the cap renders even when the app itself fails to load.\n if (budgetRes.ok) renderBudget(await budgetRes.json());\n if (!res.ok) {\n document.getElementById('banner').hidden = false;\n document.getElementById('banner').className = 'banner warn';\n document.getElementById('banner').textContent = 'Failed to load config (' + res.status + ')';\n return;\n }\n render(await res.json());\n if (manRes.ok) renderManifest(await manRes.json());\n}\n\ndocument.getElementById('save').addEventListener('click', async () => {\n const body = {};\n for (const [key, { secret, input }] of state.dirty) {\n if (secret && input.value === '') continue; // untouched secret: keep as-is\n body[key] = input.type === 'checkbox' ? input.checked : parseInput(input.value);\n }\n if (Object.keys(body).length === 0) return;\n const status = document.getElementById('status');\n status.textContent = 'Saving…';\n const res = await fetch('/api/apps/' + APP + '/config', {\n method: 'PUT',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(body),\n });\n status.textContent = res.ok ? 'Saved — app restarted.' : 'Save failed (' + res.status + ')';\n if (res.ok) await load();\n});\n\nload();\n</script>\n</body>\n</html>\n`;\n}\n","import { randomUUID } from 'node:crypto';\nimport { readdir, readFile } from 'node:fs/promises';\nimport { extname, resolve, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport {\n type AIProvider,\n type AISession,\n APP_NAME_PATTERN,\n type AppContext,\n type AppManifest,\n type FetchHandler,\n} from '@alashi/apps-sdk';\nimport { Hono } from 'hono';\nimport { streamSSE } from 'hono/streaming';\nimport {\n appBudget,\n assertWithinBudget,\n BudgetExceededError,\n budgetStatus,\n budgetStatusFor,\n setAppBudget,\n} from './budget.js';\nimport { loadApp } from './loader.js';\nimport { buildAppContext, loadAgents, loadInstructions, resolveAppConfig } from './runtime.js';\nimport { Scheduler } from './scheduler.js';\nimport { settingsPage } from './settings-page.js';\nimport type { ChatState, Conversation, Store } from './store.js';\nimport { formatUsd, UsageRecorder } from './usage.js';\n\ninterface RunningApp {\n manifest: AppManifest;\n ctx: AppContext;\n handler?: FetchHandler;\n jobHandlers?: Record<string, (ctx: AppContext) => Promise<void>>;\n}\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.ico': 'image/x-icon',\n '.txt': 'text/plain; charset=utf-8',\n '.woff2': 'font/woff2',\n};\n\nfunction escapeHtml(value: string): string {\n return value\n .replaceAll('&', '&')\n .replaceAll('<', '<')\n .replaceAll('>', '>')\n .replaceAll('\"', '"');\n}\n\n/** Host-rendered failure page — apps never need to ship their own error UI. */\nfunction appErrorPage(name: string, err: unknown): string {\n return `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>${escapeHtml(name)} — failed to load</title>\n<link rel=\"stylesheet\" href=\"/assets/v1/system.css\">\n</head>\n<body>\n<header class=\"topbar\"><strong>alashi</strong></header>\n<main class=\"narrow\">\n<h1>${escapeHtml(name)} failed to load</h1>\n<div class=\"panel\">\n<p>${escapeHtml(String(err))}</p>\n<p class=\"muted\"><a href=\"/\">Back to apps</a></p>\n</div>\n</main>\n</body>\n</html>\n`;\n}\n\nexport interface AppServer {\n /** The Hono app — pass its fetch to a Node server or call request() in tests. */\n app: Hono;\n /** Cron scheduler for manifest-declared jobs; call start() to begin firing. */\n scheduler: Scheduler;\n /** Stops the scheduler and closes all warm AI sessions. */\n close(): Promise<void>;\n}\n\n/**\n * The host HTTP surface:\n * GET / dashboard of installed apps\n * GET /settings/:name per-app settings page (config form)\n * GET /api/apps installed apps as JSON, each with todayCostUsd\n * and its daily spend cap / over-budget state\n * GET /api/apps/:name/usage recorded AI spend: today's total, a daily series,\n * and the budget status\n * GET /api/apps/:name/budget today's spend against the app's daily cap\n * PUT /api/apps/:name/budget { dailyUsd: number | null } -> set or clear the cap\n * (a host setting; apps cannot set their own)\n * POST /api/apps/:name/chat { message, conversationId? } -> SSE stream of AgentEvents\n * (omit conversationId to start a new conversation; the\n * id is echoed in the x-conversation-id response header;\n * 402 when the app is over its daily spend cap)\n * GET /api/apps/:name/chat/conversations conversations, most recent first\n * POST /api/apps/:name/chat/conversations { sessionId?, title? } -> create a conversation,\n * optionally adopting an existing provider session (e.g. an\n * app-run investigation) so chat continues where it left off\n * GET /api/apps/:name/chat/:conversationId/history past turns from the provider transcript\n * GET /api/apps/:name/jobs manifest-declared jobs with schedule/status\n * POST /api/apps/:name/jobs/:job/run trigger a job now (409 while already running)\n * GET /api/apps/:name/manifest read-only app definition: instructions, agents,\n * skills (with dir contents), allowed tools,\n * MCP servers (with configured state), jobs, model\n * GET /api/apps/:name/config declared config schema + merged values\n * (secrets masked) + readiness/integrations\n * PUT /api/apps/:name/config merge keys into the per-app config layer\n * (null deletes a key) and restart the app\n * ALL /app/:name/api/* dispatched to the app's fetch handler (prefix stripped)\n * ALL /app/:name/* a matching static file from the app's ui dir wins,\n * otherwise the app's fetch handler (/app/:name stripped),\n * otherwise 404\n */\nexport function createServer(store: Store, provider: AIProvider): AppServer {\n const apps = new Map<string, Promise<RunningApp>>();\n const sessions = new Map<string, Promise<AISession>>();\n // One recorder for the process: every app context meters its sessions\n // through it, so chat turns, cron jobs, and app-opened sessions all count.\n const usage = new UsageRecorder(store);\n\n async function startApp(name: string): Promise<RunningApp> {\n const { definition, manifest } = await loadApp(store.appRepoDir(name));\n const ctx = await buildAppContext(store, manifest, provider, usage);\n const handler = definition.routes ? await definition.routes(ctx) : undefined;\n return { manifest, ctx, handler, jobHandlers: definition.jobHandlers };\n }\n\n /** Loads an installed app once per process; undefined when not installed. */\n async function getApp(name: string): Promise<RunningApp | undefined> {\n if (!APP_NAME_PATTERN.test(name)) return undefined;\n if (!(await store.readAppRecord(name))) return undefined;\n let running = apps.get(name);\n if (!running) {\n running = startApp(name);\n running.catch(() => apps.delete(name));\n apps.set(name, running);\n }\n return running;\n }\n\n /** chat.json per app, loaded once and mutated in place so turns share one view. */\n const chatStates = new Map<string, Promise<ChatState>>();\n\n function getChatState(name: string): Promise<ChatState> {\n let state = chatStates.get(name);\n if (!state) {\n state = store.readChatState(name);\n state.catch(() => chatStates.delete(name));\n chatStates.set(name, state);\n }\n return state;\n }\n\n function getSession(name: string, running: RunningApp, convo: Conversation): Promise<AISession> {\n const key = `${name}/${convo.id}`;\n let session = sessions.get(key);\n if (!session) {\n const model = running.ctx.config.model;\n session = running.ctx.ai.createSession({\n ...(typeof model === 'string' && { model }),\n ...(convo.sessionId && { resume: convo.sessionId }),\n });\n session.catch(() => sessions.delete(key));\n sessions.set(key, session);\n }\n return session;\n }\n\n const scheduler = new Scheduler(store, getApp);\n\n /** Rebuilds the request with `prefix` stripped and hands it to the app's handler. */\n function dispatch(\n handler: FetchHandler,\n raw: Request,\n prefix: string,\n ): Response | Promise<Response> {\n const url = new URL(raw.url);\n url.pathname = url.pathname.slice(prefix.length) || '/';\n const request = new Request(url, {\n method: raw.method,\n headers: raw.headers,\n body: raw.body,\n ...(raw.body ? { duplex: 'half' } : {}),\n } as RequestInit);\n return handler(request);\n }\n\n /** A file from the app's ui dir, or undefined when there is no match. */\n async function serveStatic(\n name: string,\n manifest: AppManifest,\n reqPath: string,\n ): Promise<Response | undefined> {\n if (!manifest.ui) return undefined;\n const uiDir = resolve(store.appRepoDir(name), manifest.ui);\n let rest: string;\n try {\n rest = decodeURIComponent(reqPath.slice(`/app/${name}/`.length));\n } catch {\n return undefined;\n }\n if (rest === '' || rest.endsWith('/')) rest += 'index.html';\n const filePath = resolve(uiDir, rest);\n if (filePath !== uiDir && !filePath.startsWith(uiDir + sep)) return undefined;\n try {\n const body = await readFile(filePath);\n const type = CONTENT_TYPES[extname(filePath)] ?? 'application/octet-stream';\n return new Response(new Uint8Array(body), {\n status: 200,\n headers: { 'content-type': type },\n });\n } catch {\n return undefined;\n }\n }\n\n const app = new Hono();\n\n app.onError((err, c) => c.json({ error: String(err) }, 500));\n\n // System UI assets (design system, later vendored libs). Versioned paths:\n // breaking changes ship as /assets/v2/, existing apps keep working.\n const assetsDir = fileURLToPath(new URL('../assets', import.meta.url));\n\n app.get('/assets/*', async (c) => {\n let rest: string;\n try {\n rest = decodeURIComponent(c.req.path.slice('/assets/'.length));\n } catch {\n return c.json({ error: 'Not found' }, 404);\n }\n const filePath = resolve(assetsDir, rest);\n if (!filePath.startsWith(assetsDir + sep)) return c.json({ error: 'Not found' }, 404);\n try {\n const body = await readFile(filePath);\n const type = CONTENT_TYPES[extname(filePath)] ?? 'application/octet-stream';\n return new Response(new Uint8Array(body), {\n status: 200,\n headers: { 'content-type': type, 'cache-control': 'public, max-age=300' },\n });\n } catch {\n return c.json({ error: 'Not found' }, 404);\n }\n });\n\n /** Readiness for the dashboard badge; loadError carries the failure reason. */\n async function readiness(\n name: string,\n ): Promise<{ ready: boolean } | { loadError: string } | undefined> {\n try {\n const running = await getApp(name);\n if (!running) return undefined;\n return (await resolveAppConfig(store, running.manifest)).status;\n } catch (err) {\n return { loadError: String(err) };\n }\n }\n\n app.get('/', async (c) => {\n const records = await store.listInstalled();\n const cells = await Promise.all(\n records.map(async (r) => {\n const name = escapeHtml(r.name);\n const status = await readiness(r.name);\n const badge = !status\n ? '<span class=\"badge err\">failed to load</span>'\n : 'loadError' in status\n ? `<span class=\"badge err\" title=\"${escapeHtml(status.loadError)}\">failed to load</span>`\n : status.ready\n ? '<span class=\"badge ok\">ready</span>'\n : `<a href=\"/settings/${name}\"><span class=\"badge err\">needs setup</span></a>`;\n const budget = await budgetStatus(store, usage, r.name);\n const todayCostUsd = budget.spentUsd;\n // Over budget is a distinct, actionable state: the app still loads and\n // is still \"ready\", it just cannot open a session until the day rolls.\n const overBadge = budget.overBudget\n ? ` <a href=\"/settings/${name}\" title=\"Daily AI spend cap reached — new sessions are refused until 00:00 UTC\"><span class=\"badge err\">over budget</span></a>`\n : '';\n const cap =\n budget.dailyUsd === undefined\n ? ''\n : ` <span class=\"muted\" title=\"Daily spend cap\">/ ${formatUsd(budget.dailyUsd)}</span>`;\n const row = `<tr>\n <td><a href=\"/app/${name}/\">${name}</a></td>\n <td class=\"muted\">v${escapeHtml(r.version)}</td>\n <td class=\"muted\">${escapeHtml(r.source.type)}</td>\n <td>${badge}${overBadge}</td>\n <td class=\"muted\" style=\"text-align:right\">${formatUsd(todayCostUsd)}${cap}</td>\n <td style=\"text-align:right\"><a href=\"/settings/${name}\">settings</a></td>\n</tr>`;\n return { row, todayCostUsd };\n }),\n );\n const rows = cells.map((cell) => cell.row).join('\\n');\n const totalToday = cells.reduce((sum, cell) => sum + cell.todayCostUsd, 0);\n const body =\n records.length === 0\n ? '<div class=\"empty\">No apps installed. Run <code>alashi install <name></code>.</div>'\n : `<table>\n<thead><tr><th>App</th><th>Version</th><th>Source</th><th>Status</th><th style=\"text-align:right\" title=\"AI spend today (UTC)\">Today</th><th></th></tr></thead>\n<tbody>\n${rows}\n</tbody>\n<tfoot><tr><td colspan=\"4\" class=\"muted\">AI spend today (UTC)</td><td class=\"muted\" style=\"text-align:right\">${formatUsd(totalToday)}</td><td></td></tr></tfoot>\n</table>`;\n return c.html(\n `<!doctype html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n<title>alashi</title>\n<link rel=\"stylesheet\" href=\"/assets/v1/system.css\">\n</head>\n<body>\n<header class=\"topbar\"><strong>alashi</strong></header>\n<main class=\"narrow\">\n<h1>Apps</h1>\n<p class=\"subtitle\">Installed apps and their status.</p>\n<div class=\"panel\">\n${body}\n</div>\n</main>\n</body>\n</html>\n`,\n );\n });\n\n app.get('/api/apps', async (c) => {\n const records = await store.listInstalled();\n const apps = await Promise.all(\n records.map(async (record) => {\n const budget = await budgetStatus(store, usage, record.name);\n return {\n ...record,\n todayCostUsd: budget.spentUsd,\n overBudget: budget.overBudget,\n ...(budget.dailyUsd !== undefined && { dailyBudgetUsd: budget.dailyUsd }),\n };\n }),\n );\n return c.json({ apps });\n });\n\n /** Spend and budget are host bookkeeping: readable even when the app fails to load. */\n async function installedApp(name: string): Promise<boolean> {\n return APP_NAME_PATTERN.test(name) && (await store.readAppRecord(name)) !== undefined;\n }\n\n app.get('/api/apps/:name/usage', async (c) => {\n const name = c.req.param('name');\n if (!(await installedApp(name))) {\n return c.json({ error: `App \"${name}\" is not installed` }, 404);\n }\n const [summary, config] = await Promise.all([usage.summary(name), store.readGlobalConfig()]);\n return c.json({ ...summary, budget: budgetStatusFor(summary, appBudget(config, name)) });\n });\n\n app.get('/api/apps/:name/budget', async (c) => {\n const name = c.req.param('name');\n if (!(await installedApp(name))) {\n return c.json({ error: `App \"${name}\" is not installed` }, 404);\n }\n return c.json(await budgetStatus(store, usage, name));\n });\n\n /**\n * Sets the app's daily spend cap. A host setting, not an app config key: it\n * never touches `apps/<name>/config.json`, so the app cannot raise it via\n * PUT /api/apps/:name/config or by shipping a manifest default.\n */\n app.put('/api/apps/:name/budget', async (c) => {\n const name = c.req.param('name');\n if (!(await installedApp(name))) {\n return c.json({ error: `App \"${name}\" is not installed` }, 404);\n }\n let body: unknown;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ error: 'Body must be JSON' }, 400);\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n return c.json({ error: 'Body must be { \"dailyUsd\": number | null }' }, 400);\n }\n const { dailyUsd } = body as { dailyUsd?: unknown };\n if (dailyUsd !== null && (typeof dailyUsd !== 'number' || !Number.isFinite(dailyUsd))) {\n return c.json({ error: '\"dailyUsd\" must be a number, or null to remove the cap' }, 400);\n }\n if (typeof dailyUsd === 'number' && dailyUsd < 0) {\n return c.json({ error: '\"dailyUsd\" must not be negative' }, 400);\n }\n await setAppBudget(store, name, dailyUsd === null ? undefined : dailyUsd);\n return c.json(await budgetStatus(store, usage, name));\n });\n\n app.get('/settings/:name', async (c) => {\n const name = c.req.param('name');\n if (!APP_NAME_PATTERN.test(name) || !(await store.readAppRecord(name))) {\n return c.json({ error: `App \"${name}\" is not installed` }, 404);\n }\n return c.html(settingsPage(name));\n });\n\n app.get('/api/apps/:name/chat/conversations', async (c) => {\n const name = c.req.param('name');\n const running = await getApp(name);\n if (!running) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n const { conversations } = await getChatState(name);\n return c.json({\n conversations: [...conversations].sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)),\n });\n });\n\n app.post('/api/apps/:name/chat/conversations', async (c) => {\n const name = c.req.param('name');\n const running = await getApp(name);\n if (!running) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n\n let sessionId: unknown;\n let title: unknown;\n try {\n ({ sessionId, title } = (await c.req.json()) as { sessionId?: unknown; title?: unknown });\n } catch {\n return c.json({ error: 'Body must be JSON' }, 400);\n }\n if (sessionId !== undefined && (typeof sessionId !== 'string' || sessionId.length === 0)) {\n return c.json({ error: '\"sessionId\" must be a non-empty string' }, 400);\n }\n if (title !== undefined && typeof title !== 'string') {\n return c.json({ error: '\"title\" must be a string' }, 400);\n }\n\n const state = await getChatState(name);\n const convo: Conversation = {\n id: randomUUID(),\n title: (title ?? '').slice(0, 80) || 'New conversation',\n updatedAt: new Date().toISOString(),\n ...(sessionId !== undefined && { sessionId }),\n };\n state.conversations.push(convo);\n await store.writeChatState(name, state);\n return c.json({ conversation: convo }, 201);\n });\n\n app.get('/api/apps/:name/chat/:conversationId/history', async (c) => {\n const name = c.req.param('name');\n const running = await getApp(name);\n if (!running) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n const conversationId = c.req.param('conversationId');\n const { conversations } = await getChatState(name);\n const convo = conversations.find((conv) => conv.id === conversationId);\n if (!convo) return c.json({ error: 'Unknown conversation' }, 404);\n const messages = convo.sessionId ? ((await provider.history?.(convo.sessionId)) ?? []) : [];\n return c.json({ messages });\n });\n\n /** Drops the cached app and its warm sessions so the next request rebuilds with fresh config. */\n function restartApp(name: string): void {\n apps.delete(name);\n for (const [key, session] of sessions) {\n if (!key.startsWith(`${name}/`)) continue;\n sessions.delete(key);\n void session.then((s) => s.close()).catch(() => undefined);\n }\n }\n\n app.get('/api/apps/:name/manifest', async (c) => {\n const name = c.req.param('name');\n const running = await getApp(name);\n if (!running) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n const { manifest } = running;\n const { merged, status } = await resolveAppConfig(store, manifest);\n const repoDir = store.appRepoDir(name);\n\n const instructions = await loadInstructions(repoDir, manifest.instructions);\n\n const skills = await Promise.all(\n manifest.skills.map(async (dir) => {\n let entries: string[] = [];\n try {\n entries = (await readdir(resolve(repoDir, dir))).sort();\n } catch {\n // A missing skill dir is still worth surfacing, just with no entries.\n }\n return { dir, entries };\n }),\n );\n\n const configured = new Map(status.integrations.map((i) => [i.name, i.configured]));\n // Raw manifest declarations only: `${config.*}` placeholders are never\n // interpolated here, so secret values cannot leak through env/headers.\n const mcpServers = Object.entries(manifest.mcpServers).map(([server, decl]) => ({\n name: server,\n ...('command' in decl\n ? { type: 'stdio', command: [decl.command, ...(decl.args ?? [])].join(' ') }\n : { type: decl.type, url: decl.url }),\n optional: decl.optional === true,\n configured: configured.get(server) ?? true,\n }));\n\n return c.json({\n name: manifest.name,\n version: manifest.version,\n ...(manifest.description !== undefined && { description: manifest.description }),\n ...(instructions !== undefined && { instructions }),\n ...(typeof merged.model === 'string' && { model: merged.model }),\n agents: await loadAgents(repoDir, manifest.agents),\n skills,\n allowedTools: manifest.permissions.allowedTools,\n mcpServers,\n jobs: manifest.jobs,\n });\n });\n\n app.get('/api/apps/:name/config', async (c) => {\n const name = c.req.param('name');\n const running = await getApp(name);\n if (!running) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n const { manifest } = running;\n const { merged, status } = await resolveAppConfig(store, manifest);\n // Secret values never leave the host; the UI only learns whether they are set.\n const values: Record<string, unknown> = { ...merged };\n const secretsSet: string[] = [];\n for (const [key, decl] of Object.entries(manifest.config)) {\n if (decl.secret !== true) continue;\n if (values[key] !== undefined) secretsSet.push(key);\n delete values[key];\n }\n return c.json({\n schema: manifest.config,\n values,\n secretsSet,\n userKeys: Object.keys(await store.readAppConfig(name)),\n ...status,\n });\n });\n\n app.put('/api/apps/:name/config', async (c) => {\n const name = c.req.param('name');\n const running = await getApp(name);\n if (!running) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n\n let body: unknown;\n try {\n body = await c.req.json();\n } catch {\n return c.json({ error: 'Body must be JSON' }, 400);\n }\n if (typeof body !== 'object' || body === null || Array.isArray(body)) {\n return c.json({ error: 'Body must be an object of config keys' }, 400);\n }\n\n const config = await store.readAppConfig(name);\n for (const [key, value] of Object.entries(body)) {\n if (key === '__proto__' || key === 'constructor' || key === 'prototype') continue;\n if (value === null) delete config[key];\n else config[key] = value;\n }\n await store.writeAppConfig(name, config);\n restartApp(name);\n\n const { status } = await resolveAppConfig(store, running.manifest);\n return c.json({ ok: true, ...status });\n });\n\n app.get('/api/apps/:name/jobs', async (c) => {\n const name = c.req.param('name');\n const jobs = await scheduler.status(name);\n if (!jobs) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n return c.json({ jobs });\n });\n\n app.post('/api/apps/:name/jobs/:job/run', async (c) => {\n const result = await scheduler.run(c.req.param('name'), c.req.param('job'));\n if (result === undefined) return c.json({ error: 'Unknown app or job' }, 404);\n if (result === 'skipped') return c.json({ error: 'Job is already running' }, 409);\n return c.json({ run: result }, result.ok ? 200 : 500);\n });\n\n app.post('/api/apps/:name/chat', async (c) => {\n const name = c.req.param('name');\n const running = await getApp(name);\n if (!running) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n\n let message: unknown;\n let conversationId: unknown;\n try {\n ({ message, conversationId } = (await c.req.json()) as {\n message?: unknown;\n conversationId?: unknown;\n });\n } catch {\n return c.json({ error: 'Body must be JSON' }, 400);\n }\n if (typeof message !== 'string' || message.length === 0) {\n return c.json({ error: 'Body must be { \"message\": string }' }, 400);\n }\n if (conversationId !== undefined && typeof conversationId !== 'string') {\n return c.json({ error: '\"conversationId\" must be a string' }, 400);\n }\n\n // Chat sessions are kept warm across turns, so the createSession gate only\n // sees the first turn of a conversation. Re-check here: the same cap, one\n // level up, so a conversation that goes over mid-chat stops too.\n try {\n await assertWithinBudget(store, usage, name);\n } catch (err) {\n if (BudgetExceededError.is(err)) return c.json({ error: err.message }, 402);\n throw err;\n }\n\n const state = await getChatState(name);\n let convo: Conversation | undefined;\n if (conversationId !== undefined) {\n convo = state.conversations.find((conv) => conv.id === conversationId);\n if (!convo) return c.json({ error: 'Unknown conversation' }, 404);\n } else {\n convo = {\n id: randomUUID(),\n title: message.slice(0, 80),\n updatedAt: new Date().toISOString(),\n };\n state.conversations.push(convo);\n await store.writeChatState(name, state);\n }\n const conversation = convo;\n\n let session: AISession;\n try {\n session = await getSession(name, running, conversation);\n } catch (err) {\n return c.json({ error: `Failed to start AI session: ${String(err)}` }, 502);\n }\n\n c.header('x-conversation-id', conversation.id);\n return streamSSE(c, async (stream) => {\n try {\n for await (const event of session.send(message)) {\n // One small write per turn: tracks the provider session and bumps recency.\n if (event.type === 'session') {\n conversation.sessionId = event.sessionId;\n conversation.updatedAt = new Date().toISOString();\n await store.writeChatState(name, state);\n }\n await stream.writeSSE({ event: event.type, data: JSON.stringify(event) });\n }\n } catch (err) {\n await stream.writeSSE({\n event: 'result',\n data: JSON.stringify({ type: 'result', ok: false, error: String(err) }),\n });\n }\n });\n });\n\n app.all('/app/:name/api/*', async (c) => {\n const name = c.req.param('name');\n const running = await getApp(name);\n if (!running) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n if (!running.handler) return c.json({ error: `App \"${name}\" has no routes` }, 404);\n return dispatch(running.handler, c.req.raw, `/app/${name}/api`);\n });\n\n // Relative asset URLs in app UIs only work with a trailing slash.\n app.get('/app/:name', (c) => c.redirect(`/app/${c.req.param('name')}/`));\n\n app.all('/app/:name/*', async (c) => {\n const name = c.req.param('name');\n let running: RunningApp | undefined;\n try {\n running = await getApp(name);\n } catch (err) {\n // Browser navigations get a host-rendered page; API-ish requests keep JSON.\n if (c.req.method === 'GET' && (c.req.header('accept') ?? '').includes('text/html')) {\n return c.html(appErrorPage(name, err), 500);\n }\n throw err;\n }\n if (!running) return c.json({ error: `App \"${name}\" is not installed` }, 404);\n\n if (c.req.method === 'GET' || c.req.method === 'HEAD') {\n const file = await serveStatic(name, running.manifest, c.req.path);\n if (file) return file;\n }\n if (running.handler) return dispatch(running.handler, c.req.raw, `/app/${name}`);\n return c.json({ error: 'Not found' }, 404);\n });\n\n return {\n app,\n scheduler,\n async close() {\n scheduler.stop();\n const open = [...sessions.values()];\n sessions.clear();\n await Promise.all(\n open.map(async (p) => {\n try {\n await (await p).close();\n } catch {\n // Session may already be gone; closing is best-effort.\n }\n }),\n );\n },\n };\n}\n","import { mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\nimport { APP_NAME_PATTERN, AppSdkError, type ConfigObject } from '@alashi/apps-sdk';\nimport type { AppSourceSpec } from './sources.js';\n\nexport interface RegistryRef {\n name: string;\n url: string;\n}\n\n/** Host-enforced spend limits for one app. */\nexport interface AppBudget {\n /**\n * Daily AI spend ceiling in USD. Once the app's spend for the UTC day\n * reaches it, the host refuses `createSession`. `0` means \"no AI at all\".\n */\n dailyUsd: number;\n}\n\nexport interface GlobalConfig {\n registries: RegistryRef[];\n port?: number;\n /** Bind address for `alashi serve`; defaults to loopback (the dashboard has no auth). */\n host?: string;\n /** Per-app global overrides, keyed by app name (middle config layer). */\n apps?: Record<string, ConfigObject>;\n /**\n * Host-enforced per-app spend caps, keyed by app name.\n *\n * Deliberately a sibling of `apps` rather than a key inside it: everything in\n * `apps` is merged into the app's config and can be overridden by the app's\n * own `config.json` layer, which would let an app raise its own ceiling.\n * A cap is enforcement, so it lives where only the host can set it.\n */\n budgets?: Record<string, AppBudget>;\n}\n\nexport interface Conversation {\n id: string;\n /** Provider session backing this conversation; set after the first turn. */\n sessionId?: string;\n title?: string;\n updatedAt: string;\n}\n\n/** Host-side chat state for an app; the transcripts themselves live with the AI provider. */\nexport interface ChatState {\n conversations: Conversation[];\n}\n\nexport interface JobRun {\n job: string;\n trigger: 'schedule' | 'catchup' | 'manual';\n startedAt: string;\n finishedAt: string;\n ok: boolean;\n error?: string;\n}\n\n/** Host-side job state for an app: last run start per job plus a capped run log. */\nexport interface JobState {\n lastRun: Record<string, string>;\n runs: JobRun[];\n}\n\n/** One UTC day of AI spend for an app. */\nexport interface UsageDay {\n /** UTC calendar day, `YYYY-MM-DD`. */\n date: string;\n /** Sum of the `costUsd` the provider reported for that day's turns. */\n costUsd: number;\n /** Turns that reported a cost (turns without one are not counted). */\n turns: number;\n}\n\n/** Host-side AI spend for an app, oldest day first, trimmed to the retention window. */\nexport interface UsageState {\n days: UsageDay[];\n}\n\nexport interface InstallRecord {\n name: string;\n version: string;\n /** The SDK range the app's manifest declared at install/update time. */\n sdkVersion?: string;\n source: AppSourceSpec;\n installedAt: string;\n updatedAt: string;\n}\n\nasync function readJson<T>(path: string): Promise<T | undefined> {\n try {\n return JSON.parse(await readFile(path, 'utf8')) as T;\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return undefined;\n throw err;\n }\n}\n\nasync function writeJson(path: string, value: unknown): Promise<void> {\n await mkdir(join(path, '..'), { recursive: true });\n const tmp = `${path}.tmp`;\n await writeFile(tmp, `${JSON.stringify(value, null, 2)}\\n`, 'utf8');\n await rename(tmp, path);\n}\n\n/**\n * The portable state directory ($ALASHI_HOME, default ~/.alashi).\n * Everything the host knows lives under this root so it can be moved\n * between machines as a unit.\n */\nexport class Store {\n readonly root: string;\n\n constructor(root?: string) {\n this.root = root ?? process.env.ALASHI_HOME ?? join(homedir(), '.alashi');\n }\n\n get configPath(): string {\n return join(this.root, 'config.json');\n }\n\n get appsDir(): string {\n return join(this.root, 'apps');\n }\n\n get registriesDir(): string {\n return join(this.root, 'registries');\n }\n\n get stagingDir(): string {\n return join(this.root, '.staging');\n }\n\n /** Validates the name before using it in a path (traversal guard). */\n appDir(name: string): string {\n if (!APP_NAME_PATTERN.test(name)) {\n throw new AppSdkError(`Invalid app name: ${JSON.stringify(name)}`);\n }\n return join(this.appsDir, name);\n }\n\n appRepoDir(name: string): string {\n return join(this.appDir(name), 'repo');\n }\n\n appDataDir(name: string): string {\n return join(this.appDir(name), 'data');\n }\n\n appConfigPath(name: string): string {\n return join(this.appDir(name), 'config.json');\n }\n\n appRecordPath(name: string): string {\n return join(this.appDir(name), 'app.json');\n }\n\n appChatStatePath(name: string): string {\n return join(this.appDir(name), 'chat.json');\n }\n\n appJobStatePath(name: string): string {\n return join(this.appDir(name), 'jobs.json');\n }\n\n appUsagePath(name: string): string {\n return join(this.appDir(name), 'usage.json');\n }\n\n registryCachePath(name: string): string {\n if (!APP_NAME_PATTERN.test(name)) {\n throw new AppSdkError(`Invalid registry name: ${JSON.stringify(name)}`);\n }\n return join(this.registriesDir, `${name}.json`);\n }\n\n async readGlobalConfig(): Promise<GlobalConfig> {\n return (await readJson<GlobalConfig>(this.configPath)) ?? { registries: [] };\n }\n\n async writeGlobalConfig(config: GlobalConfig): Promise<void> {\n await writeJson(this.configPath, config);\n }\n\n async readAppRecord(name: string): Promise<InstallRecord | undefined> {\n return readJson<InstallRecord>(this.appRecordPath(name));\n }\n\n async writeAppRecord(record: InstallRecord): Promise<void> {\n await writeJson(this.appRecordPath(record.name), record);\n }\n\n async readAppConfig(name: string): Promise<ConfigObject> {\n return (await readJson<ConfigObject>(this.appConfigPath(name))) ?? {};\n }\n\n async writeAppConfig(name: string, config: ConfigObject): Promise<void> {\n await writeJson(this.appConfigPath(name), config);\n }\n\n async readChatState(name: string): Promise<ChatState> {\n const state = await readJson<Partial<ChatState>>(this.appChatStatePath(name));\n return { conversations: state?.conversations ?? [] };\n }\n\n async writeChatState(name: string, state: ChatState): Promise<void> {\n await writeJson(this.appChatStatePath(name), state);\n }\n\n async readJobState(name: string): Promise<JobState> {\n const state = await readJson<Partial<JobState>>(this.appJobStatePath(name));\n return { lastRun: state?.lastRun ?? {}, runs: state?.runs ?? [] };\n }\n\n async writeJobState(name: string, state: JobState): Promise<void> {\n await writeJson(this.appJobStatePath(name), state);\n }\n\n async readUsageState(name: string): Promise<UsageState> {\n const state = await readJson<Partial<UsageState>>(this.appUsagePath(name));\n return { days: state?.days ?? [] };\n }\n\n async writeUsageState(name: string, state: UsageState): Promise<void> {\n await writeJson(this.appUsagePath(name), state);\n }\n\n async listInstalled(): Promise<InstallRecord[]> {\n let entries: string[];\n try {\n entries = await readdir(this.appsDir);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code === 'ENOENT') return [];\n throw err;\n }\n const records: InstallRecord[] = [];\n for (const entry of entries.sort()) {\n if (!APP_NAME_PATTERN.test(entry)) continue;\n const record = await this.readAppRecord(entry);\n if (record) records.push(record);\n }\n return records;\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -73,6 +73,10 @@ the connection before calling `runMigrations`.
|
|
|
73
73
|
with `openConversation`/`adoptSession`). Methods: `adoptSession(sessionId, title?)`,
|
|
74
74
|
`openConversation(id)`, `newChat()`. Bubbling events: `alashi-chat:turn`
|
|
75
75
|
(`{ok, costUsd, error, conversationId}`), `alashi-chat:conversation`.
|
|
76
|
+
The same script also registers `<alashi-markdown>` — the host's markdown
|
|
77
|
+
renderer as a standalone display primitive, for prose outside a conversation
|
|
78
|
+
(`node.text = card.description` re-renders on assignment; `text="**hi**"` is
|
|
79
|
+
honoured on connect for static markup). No fetching, no conversation logic.
|
|
76
80
|
Never vendor or fork system UI; breaking changes ship as `/assets/v2/`.
|
|
77
81
|
|
|
78
82
|
## App routes
|
|
@@ -84,7 +88,8 @@ the connection before calling `runMigrations`.
|
|
|
84
88
|
|
|
85
89
|
- Never `innerHTML` external or model-derived data in the UI — build DOM via
|
|
86
90
|
the `el()` helper / `textContent`. Markdown rendering belongs to
|
|
87
|
-
`<alashi-chat>`,
|
|
91
|
+
`<alashi-chat>` / `<alashi-markdown>`, the sanctioned exception because the
|
|
92
|
+
host's renderer is escape-first. Never bundle a markdown library.
|
|
88
93
|
- Validate at boundaries (webhooks, user input): JSON only, cap body sizes,
|
|
89
94
|
4xx on garbage. Model output is a boundary too — parse defensively.
|
|
90
95
|
- Secrets come from declared `config` keys (`secret: true`), referenced as
|