@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 CHANGED
@@ -1,4 +1,4 @@
1
- /* alashi chat component v1
1
+ /* alashi chat + markdown components v1
2
2
  *
3
3
  * The system chat UI, served by the host. Apps drop in:
4
4
  *
@@ -9,6 +9,10 @@
9
9
  * and cost display. Renders into light DOM using system.css classes, so it
10
10
  * inherits the app's theme tokens.
11
11
  *
12
+ * The same script also registers <alashi-markdown>, the markdown renderer on
13
+ * its own for apps that display prose outside a conversation (see the bottom
14
+ * of this file). One renderer, one place formatting bugs get fixed.
15
+ *
12
16
  * Attributes:
13
17
  * app app name (default: derived from /app/<name>/ in the URL)
14
18
  * placeholder input placeholder text
@@ -31,7 +35,8 @@
31
35
  * Breaking changes ship as /assets/v2/chat.js. */
32
36
 
33
37
  (() => {
34
- if (customElements.get('alashi-chat')) return;
38
+ // Loading the script twice is a no-op for every element it defines.
39
+ if (customElements.get('alashi-chat') || customElements.get('alashi-markdown')) return;
35
40
 
36
41
  const escapeHtml = (s) =>
37
42
  s
@@ -357,5 +362,40 @@
357
362
  }
358
363
  }
359
364
 
365
+ /* <alashi-markdown> — the same renderMarkdown() as chat bubbles, as a
366
+ * standalone display primitive. No fetching, no SSE, no conversations.
367
+ *
368
+ * <alashi-markdown text="**hi**"></alashi-markdown> // static markup
369
+ * node.text = card.description; // re-renders
370
+ *
371
+ * The `text` property is the primary path: apps pass model-derived,
372
+ * multi-line strings that do not belong in an attribute. Renders into light
373
+ * DOM (like _setMsgText) so it inherits the app's theme tokens, and carries
374
+ * the `markdown` class so system.css can style its children. */
375
+ class AlashiMarkdown extends HTMLElement {
376
+ connectedCallback() {
377
+ this.classList.add('markdown');
378
+ // A property set before connect wins; the attribute is the static path.
379
+ if (this._text === undefined) this._text = this.getAttribute('text') ?? '';
380
+ this._render();
381
+ }
382
+
383
+ get text() {
384
+ return this._text ?? '';
385
+ }
386
+
387
+ set text(value) {
388
+ this._text = String(value ?? '');
389
+ this._render();
390
+ }
391
+
392
+ _render() {
393
+ // Escape-first pipeline: renderMarkdown escapes before rebuilding a
394
+ // known-safe subset of tags, so untrusted markdown stays inert.
395
+ this.innerHTML = renderMarkdown(String(this._text ?? ''));
396
+ }
397
+ }
398
+
360
399
  customElements.define('alashi-chat', AlashiChat);
400
+ customElements.define('alashi-markdown', AlashiMarkdown);
361
401
  })();
@@ -578,41 +578,39 @@ input[type="checkbox"] {
578
578
  /* assistant bubbles hold rendered markdown (chat.js), not preformatted text */
579
579
  white-space: normal;
580
580
  }
581
- .msg.assistant p,
582
- .msg.assistant ul,
583
- .msg.assistant ol,
584
- .msg.assistant pre,
585
- .msg.assistant h3,
586
- .msg.assistant h4,
587
- .msg.assistant h5,
588
- .msg.assistant h6 {
581
+ /* Rendered-markdown prose. Shared by assistant chat bubbles and the
582
+ * <alashi-markdown> element (or any `.markdown` container) — chat.js renders
583
+ * both with the same renderMarkdown(), so the styling lives in one place too.
584
+ * Headings stay h3..h6 because that is what renderMarkdown emits. */
585
+ alashi-markdown {
586
+ /* unknown element to CSS, same as <alashi-chat> in consumer apps */
587
+ display: block;
588
+ }
589
+ :is(.msg.assistant, alashi-markdown, .markdown) :is(p, ul, ol, pre, h3, h4, h5, h6) {
589
590
  margin: 0 0 0.6rem;
590
591
  }
591
- .msg.assistant > :last-child {
592
+ :is(.msg.assistant, alashi-markdown, .markdown) > :last-child {
592
593
  margin-bottom: 0;
593
594
  }
594
- .msg.assistant ul,
595
- .msg.assistant ol {
595
+ :is(.msg.assistant, alashi-markdown, .markdown) :is(ul, ol) {
596
596
  padding-left: 1.3rem;
597
597
  }
598
- .msg.assistant li {
598
+ :is(.msg.assistant, alashi-markdown, .markdown) li {
599
599
  margin: 0.15rem 0;
600
600
  }
601
- .msg.assistant img {
601
+ :is(.msg.assistant, alashi-markdown, .markdown) img {
602
602
  max-width: 100%;
603
603
  border-radius: var(--radius-sm);
604
604
  border: 1px solid var(--border);
605
605
  }
606
- .msg.assistant pre {
606
+ :is(.msg.assistant, alashi-markdown, .markdown) pre {
607
607
  background: var(--panel);
608
608
  padding: 0.6rem 0.75rem;
609
609
  }
610
- .msg.assistant h3 {
610
+ :is(.msg.assistant, alashi-markdown, .markdown) h3 {
611
611
  font-size: 1rem;
612
612
  }
613
- .msg.assistant h4,
614
- .msg.assistant h5,
615
- .msg.assistant h6 {
613
+ :is(.msg.assistant, alashi-markdown, .markdown) :is(h4, h5, h6) {
616
614
  font-size: 0.92rem;
617
615
  }
618
616
  .msg.assistant.pending {
package/dist/cli.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { a as installApp, c as addRegistry, d as removeRegistry, g as loadApp, i as resolveAppConfig, n as createServer, o as removeApp, s as updateApp, t as Store } from "./store-Cxxnjq70.mjs";
2
+ import { A as UsageRecorder, E as budgetStatus, O as setAppBudget, b as loadApp, c as installApp, d as updateApp, f as addRegistry, h as removeRegistry, i as resolveAppConfig, n as createServer, o as exportApp, s as importApp, t as Store, u as removeApp } from "./store-VoJe_U1Q.mjs";
3
3
  import { APP_NAME_PATTERN, AppSdkError } from "@alashi/apps-sdk";
4
4
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
5
- import { join, resolve } from "node:path";
5
+ import { isAbsolute, join, relative, resolve } from "node:path";
6
6
  import { fileURLToPath } from "node:url";
7
7
  import { ClaudeCodeProvider } from "@alashi/provider-claude";
8
8
  import { serve } from "@hono/node-server";
@@ -109,10 +109,39 @@ program.command("remove").argument("<name>", "installed app name").option("--pur
109
109
  await removeApp(store, name, { purge: options.purge ?? false });
110
110
  console.log(options.purge ? `Removed ${name} (purged)` : `Removed ${name} (data kept)`);
111
111
  });
112
+ /** Prints archive paths the way the user would type them. */
113
+ function displayPath(path) {
114
+ const rel = relative(process.cwd(), path);
115
+ return rel === "" || rel.startsWith("..") || isAbsolute(rel) ? path : `./${rel}`;
116
+ }
117
+ program.command("export").argument("<name>", "installed app name").option("-o, --out <file>", "archive path (default: ./<name>-<version>.tar.gz)").description("Pack an app container and its chat transcripts into a tar.gz").action(async (name, options) => {
118
+ const result = await exportApp(store, new ClaudeCodeProvider(), name, { ...options.out && { out: options.out } });
119
+ for (const warning of result.warnings) console.log(` warning: ${warning}`);
120
+ console.log(`Exported ${result.record.name}@${result.record.version} -> ${displayPath(result.archivePath)} (${result.transcriptCount} transcripts)`);
121
+ if (result.record.source.type === "local") console.log(" note: repo/ was captured as real files, not as the local symlink");
122
+ });
123
+ async function importArchive(file) {
124
+ const result = await importApp(store, new ClaudeCodeProvider(), file);
125
+ for (const warning of result.warnings) console.log(` warning: ${warning}`);
126
+ console.log(`Imported ${result.record.name}@${result.record.version} (${result.transcriptCount} transcripts restored)`);
127
+ console.log(` data: ${store.appDataDir(result.record.name)}`);
128
+ console.log(` config: ${store.appConfigPath(result.record.name)}`);
129
+ }
130
+ program.command("import").argument("<file>", "archive produced by alashi export").description("Restore an app container and its chat transcripts from a tar.gz").action(importArchive);
112
131
  program.command("list").option("--json", "machine-readable output").description("List installed apps").action(async (options) => {
132
+ const usage = new UsageRecorder(store);
113
133
  const records = await store.listInstalled();
134
+ const spend = new Map(await Promise.all(records.map(async (record) => [record.name, await budgetStatus(store, usage, record.name)])));
114
135
  if (options.json) {
115
- console.log(JSON.stringify(records, null, 2));
136
+ console.log(JSON.stringify(records.map((record) => {
137
+ const budget = spend.get(record.name);
138
+ return {
139
+ ...record,
140
+ todayCostUsd: budget?.spentUsd ?? 0,
141
+ overBudget: budget?.overBudget ?? false,
142
+ ...budget?.dailyUsd !== void 0 && { dailyBudgetUsd: budget.dailyUsd }
143
+ };
144
+ }), null, 2));
116
145
  return;
117
146
  }
118
147
  if (records.length === 0) {
@@ -121,8 +150,27 @@ program.command("list").option("--json", "machine-readable output").description(
121
150
  }
122
151
  for (const record of records) {
123
152
  const origin = record.source.type === "git" ? record.source.url : record.source.path;
124
- console.log(`${record.name}@${record.version} ${record.source.type} ${origin}`);
153
+ const budget = spend.get(record.name);
154
+ const today = (budget?.spentUsd ?? 0).toFixed(4);
155
+ const cap = budget?.dailyUsd === void 0 ? "" : ` of $${budget.dailyUsd.toFixed(2)} cap${budget.overBudget ? " (OVER BUDGET)" : ""}`;
156
+ console.log(`${record.name}@${record.version} ${record.source.type} ${origin} $${today} today${cap}`);
157
+ }
158
+ });
159
+ program.command("budget").argument("<name>", "installed app name").argument("[dailyUsd]", "daily cap in USD, or \"none\" to remove it (omit to just show)").description("Show or set an app's host-enforced daily AI spend cap").action(async (name, dailyUsd) => {
160
+ if (!await store.readAppRecord(name)) throw new Error(`App "${name}" is not installed`);
161
+ if (dailyUsd !== void 0) {
162
+ const amount = dailyUsd === "none" || dailyUsd === "off" ? void 0 : Number(dailyUsd);
163
+ if (amount !== void 0 && (!Number.isFinite(amount) || amount < 0)) throw new Error(`Invalid daily spend cap: ${dailyUsd}`);
164
+ await setAppBudget(store, name, amount);
165
+ }
166
+ const status = await budgetStatus(store, new UsageRecorder(store), name);
167
+ const spent = `$${status.spentUsd.toFixed(4)} spent on ${status.date} (UTC)`;
168
+ if (status.dailyUsd === void 0) {
169
+ console.log(`${name}: no daily cap — ${spent}`);
170
+ return;
125
171
  }
172
+ console.log(`${name}: $${status.dailyUsd.toFixed(2)} daily cap — ${spent}, $${(status.remainingUsd ?? 0).toFixed(4)} left`);
173
+ if (status.overBudget) console.log(" OVER BUDGET: new AI sessions are refused until 00:00 UTC");
126
174
  });
127
175
  const registry = program.command("registry").description("Manage app registries");
128
176
  registry.command("add").argument("<name>", "registry name").argument("<url>", "index.json URL or git repo URL").action(async (name, url) => {
package/dist/cli.mjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.mjs","names":[],"sources":["../src/create.ts","../src/cli.ts"],"sourcesContent":["import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { APP_NAME_PATTERN, AppSdkError } from '@alashi/apps-sdk';\n\n// Works from both src/ (tests) and dist/ (published build) — the templates\n// dir sits one level up in the package root either way.\nconst TEMPLATE_DIR = fileURLToPath(new URL('../templates/basic-app', import.meta.url));\n\n// npm strips .gitignore from published packages, so it ships unprefixed.\nconst RENAMES: Record<string, string> = { gitignore: '.gitignore' };\n\n/**\n * Scaffolds a new app from the basic-app template into targetDir,\n * substituting the app name. Returns the created files, relative to\n * targetDir. Refuses to write into a non-empty directory.\n */\nexport async function scaffoldApp(name: string, targetDir: string): Promise<string[]> {\n if (!APP_NAME_PATTERN.test(name)) {\n throw new AppSdkError(\n `Invalid app name \"${name}\": use lowercase letters, digits, and hyphens (max 64 chars)`,\n );\n }\n const existing = await readdir(targetDir).catch(() => undefined);\n if (existing && existing.length > 0) {\n throw new AppSdkError(`Directory is not empty: ${targetDir}`);\n }\n\n const written: string[] = [];\n async function copyDir(rel: string): Promise<void> {\n const entries = await readdir(join(TEMPLATE_DIR, rel), { withFileTypes: true });\n await mkdir(join(targetDir, rel), { recursive: true });\n for (const entry of entries) {\n const relPath = join(rel, entry.name);\n if (entry.isDirectory()) {\n await copyDir(relPath);\n continue;\n }\n const content = await readFile(join(TEMPLATE_DIR, relPath), 'utf8');\n const outPath = join(rel, RENAMES[entry.name] ?? entry.name);\n await writeFile(join(targetDir, outPath), content.replaceAll('__APP_NAME__', name), 'utf8');\n written.push(outPath);\n }\n }\n await copyDir('');\n return written.sort();\n}\n","#!/usr/bin/env node\nimport { resolve } from 'node:path';\nimport { ClaudeCodeProvider } from '@alashi/provider-claude';\nimport { serve } from '@hono/node-server';\nimport { Command } from 'commander';\nimport { scaffoldApp } from './create.js';\nimport { installApp, removeApp, updateApp } from './installer.js';\nimport { loadApp } from './loader.js';\nimport { addRegistry, removeRegistry } from './registry.js';\nimport { resolveAppConfig } from './runtime.js';\nimport { createServer } from './server.js';\nimport { Store } from './store.js';\n\nconst program = new Command();\nconst store = new Store();\n\nprogram.name('alashi').description('Install, manage, and run Claude Code-powered apps');\n\nasync function serveDashboard(portArg?: string, hostArg?: string): Promise<void> {\n const config = await store.readGlobalConfig();\n const port = portArg ? Number(portArg) : (config.port ?? 4141);\n if (!Number.isInteger(port) || port < 0 || port > 65535) {\n throw new Error(`Invalid port: ${portArg}`);\n }\n // Loopback by default: the dashboard has no auth, so it must not be\n // LAN-reachable unless the user explicitly opts in with --host.\n const hostname = hostArg ?? config.host ?? '127.0.0.1';\n const server = createServer(store, new ClaudeCodeProvider());\n await server.scheduler.start();\n const httpServer = serve({ fetch: server.app.fetch, port, hostname }, (info) => {\n console.log(`alashi dashboard at http://localhost:${info.port}`);\n if (hostname !== '127.0.0.1' && hostname !== 'localhost') {\n console.log(` WARNING: listening on ${hostname} — the dashboard has no authentication`);\n }\n });\n const shutdown = (): void => {\n httpServer.close();\n void server.close().finally(() => process.exit(0));\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n}\n\nprogram\n .command('serve', { isDefault: true })\n .option('-p, --port <port>', 'port to listen on (default: config port or 4141)')\n .option('--host <host>', 'address to bind (default: 127.0.0.1; the dashboard has no auth)')\n .description('Serve the web dashboard (the default when no command is given)')\n .action(async (options: { port?: string; host?: string }) =>\n serveDashboard(options.port, options.host),\n );\n\nprogram\n .command('create')\n .argument('<name>', 'app name (lowercase letters, digits, hyphens)')\n .argument('[directory]', 'target directory (default: ./<name>)')\n .description('Scaffold a new app from the basic template')\n .action(async (name: string, directory?: string) => {\n const target = directory ?? name;\n const files = await scaffoldApp(name, resolve(target));\n console.log(`Created ${name}/`);\n for (const file of files) console.log(` ${file}`);\n console.log('\\nNext steps:');\n console.log(` cd ${target}`);\n console.log(' npm install');\n console.log(' alashi install .');\n console.log(' alashi # dashboard at http://localhost:4141');\n });\n\nprogram\n .command('install')\n .argument('<source>', 'app name (via registry), git URL, or local path')\n .description('Install an app')\n .action(async (source: string) => {\n const record = await installApp(store, source);\n console.log(`Installed ${record.name}@${record.version} (${record.source.type})`);\n console.log(` data: ${store.appDataDir(record.name)}`);\n console.log(` config: ${store.appConfigPath(record.name)}`);\n await printSetupHints(record.name);\n });\n\n/** After install: what still needs configuring, and which integrations exist. */\nasync function printSetupHints(name: string): Promise<void> {\n let manifest: Awaited<ReturnType<typeof loadApp>>['manifest'];\n try {\n ({ manifest } = await loadApp(store.appRepoDir(name)));\n } catch {\n return; // install already succeeded; hints are best-effort\n }\n const { status } = await resolveAppConfig(store, manifest);\n if (status.missing.length > 0) {\n console.log(` requires: ${status.missing.join(', ')}`);\n }\n const optional = status.integrations.filter((i) => i.optional && !i.configured);\n if (optional.length > 0) {\n const list = optional.map((i) => `${i.name} (needs ${i.missing.join(', ')})`).join(', ');\n console.log(` optional integrations: ${list}`);\n }\n if (status.missing.length > 0 || optional.length > 0) {\n console.log(` configure via the dashboard settings page or the config file above`);\n }\n}\n\nprogram\n .command('update')\n .argument('<name>', 'installed app name')\n .description('Update an installed app')\n .action(async (name: string) => {\n const record = await updateApp(store, name);\n console.log(`Updated ${record.name} to ${record.version}`);\n });\n\nprogram\n .command('remove')\n .argument('<name>', 'installed app name')\n .option('--purge', 'also delete the app data and config')\n .description('Remove an installed app (keeps data unless --purge)')\n .action(async (name: string, options: { purge?: boolean }) => {\n await removeApp(store, name, { purge: options.purge ?? false });\n console.log(options.purge ? `Removed ${name} (purged)` : `Removed ${name} (data kept)`);\n });\n\nprogram\n .command('list')\n .option('--json', 'machine-readable output')\n .description('List installed apps')\n .action(async (options: { json?: boolean }) => {\n const records = await store.listInstalled();\n if (options.json) {\n console.log(JSON.stringify(records, null, 2));\n return;\n }\n if (records.length === 0) {\n console.log('No apps installed. Try: alashi install <name|git-url|path>');\n return;\n }\n for (const record of records) {\n const origin = record.source.type === 'git' ? record.source.url : record.source.path;\n console.log(`${record.name}@${record.version} ${record.source.type} ${origin}`);\n }\n });\n\nconst registry = program.command('registry').description('Manage app registries');\n\nregistry\n .command('add')\n .argument('<name>', 'registry name')\n .argument('<url>', 'index.json URL or git repo URL')\n .action(async (name: string, url: string) => {\n const index = await addRegistry(store, name, url);\n console.log(`Added registry \"${name}\" (${index.apps.length} apps)`);\n });\n\nregistry\n .command('remove')\n .argument('<name>', 'registry name')\n .action(async (name: string) => {\n await removeRegistry(store, name);\n console.log(`Removed registry \"${name}\"`);\n });\n\nregistry.command('list').action(async () => {\n const config = await store.readGlobalConfig();\n if (config.registries.length === 0) {\n console.log('No registries configured.');\n return;\n }\n for (const r of config.registries) console.log(`${r.name} ${r.url}`);\n});\n\nprogram.parseAsync().catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : err);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
1
+ {"version":3,"file":"cli.mjs","names":[],"sources":["../src/create.ts","../src/cli.ts"],"sourcesContent":["import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { APP_NAME_PATTERN, AppSdkError } from '@alashi/apps-sdk';\n\n// Works from both src/ (tests) and dist/ (published build) — the templates\n// dir sits one level up in the package root either way.\nconst TEMPLATE_DIR = fileURLToPath(new URL('../templates/basic-app', import.meta.url));\n\n// npm strips .gitignore from published packages, so it ships unprefixed.\nconst RENAMES: Record<string, string> = { gitignore: '.gitignore' };\n\n/**\n * Scaffolds a new app from the basic-app template into targetDir,\n * substituting the app name. Returns the created files, relative to\n * targetDir. Refuses to write into a non-empty directory.\n */\nexport async function scaffoldApp(name: string, targetDir: string): Promise<string[]> {\n if (!APP_NAME_PATTERN.test(name)) {\n throw new AppSdkError(\n `Invalid app name \"${name}\": use lowercase letters, digits, and hyphens (max 64 chars)`,\n );\n }\n const existing = await readdir(targetDir).catch(() => undefined);\n if (existing && existing.length > 0) {\n throw new AppSdkError(`Directory is not empty: ${targetDir}`);\n }\n\n const written: string[] = [];\n async function copyDir(rel: string): Promise<void> {\n const entries = await readdir(join(TEMPLATE_DIR, rel), { withFileTypes: true });\n await mkdir(join(targetDir, rel), { recursive: true });\n for (const entry of entries) {\n const relPath = join(rel, entry.name);\n if (entry.isDirectory()) {\n await copyDir(relPath);\n continue;\n }\n const content = await readFile(join(TEMPLATE_DIR, relPath), 'utf8');\n const outPath = join(rel, RENAMES[entry.name] ?? entry.name);\n await writeFile(join(targetDir, outPath), content.replaceAll('__APP_NAME__', name), 'utf8');\n written.push(outPath);\n }\n }\n await copyDir('');\n return written.sort();\n}\n","#!/usr/bin/env node\nimport { isAbsolute, relative, resolve } from 'node:path';\nimport { ClaudeCodeProvider } from '@alashi/provider-claude';\nimport { serve } from '@hono/node-server';\nimport { Command } from 'commander';\nimport { budgetStatus, setAppBudget } from './budget.js';\nimport { scaffoldApp } from './create.js';\nimport { installApp, removeApp, updateApp } from './installer.js';\nimport { loadApp } from './loader.js';\nimport { exportApp, importApp } from './portability.js';\nimport { addRegistry, removeRegistry } from './registry.js';\nimport { resolveAppConfig } from './runtime.js';\nimport { createServer } from './server.js';\nimport { Store } from './store.js';\nimport { UsageRecorder } from './usage.js';\n\nconst program = new Command();\nconst store = new Store();\n\nprogram.name('alashi').description('Install, manage, and run Claude Code-powered apps');\n\nasync function serveDashboard(portArg?: string, hostArg?: string): Promise<void> {\n const config = await store.readGlobalConfig();\n const port = portArg ? Number(portArg) : (config.port ?? 4141);\n if (!Number.isInteger(port) || port < 0 || port > 65535) {\n throw new Error(`Invalid port: ${portArg}`);\n }\n // Loopback by default: the dashboard has no auth, so it must not be\n // LAN-reachable unless the user explicitly opts in with --host.\n const hostname = hostArg ?? config.host ?? '127.0.0.1';\n const server = createServer(store, new ClaudeCodeProvider());\n await server.scheduler.start();\n const httpServer = serve({ fetch: server.app.fetch, port, hostname }, (info) => {\n console.log(`alashi dashboard at http://localhost:${info.port}`);\n if (hostname !== '127.0.0.1' && hostname !== 'localhost') {\n console.log(` WARNING: listening on ${hostname} — the dashboard has no authentication`);\n }\n });\n const shutdown = (): void => {\n httpServer.close();\n void server.close().finally(() => process.exit(0));\n };\n process.on('SIGINT', shutdown);\n process.on('SIGTERM', shutdown);\n}\n\nprogram\n .command('serve', { isDefault: true })\n .option('-p, --port <port>', 'port to listen on (default: config port or 4141)')\n .option('--host <host>', 'address to bind (default: 127.0.0.1; the dashboard has no auth)')\n .description('Serve the web dashboard (the default when no command is given)')\n .action(async (options: { port?: string; host?: string }) =>\n serveDashboard(options.port, options.host),\n );\n\nprogram\n .command('create')\n .argument('<name>', 'app name (lowercase letters, digits, hyphens)')\n .argument('[directory]', 'target directory (default: ./<name>)')\n .description('Scaffold a new app from the basic template')\n .action(async (name: string, directory?: string) => {\n const target = directory ?? name;\n const files = await scaffoldApp(name, resolve(target));\n console.log(`Created ${name}/`);\n for (const file of files) console.log(` ${file}`);\n console.log('\\nNext steps:');\n console.log(` cd ${target}`);\n console.log(' npm install');\n console.log(' alashi install .');\n console.log(' alashi # dashboard at http://localhost:4141');\n });\n\nprogram\n .command('install')\n .argument('<source>', 'app name (via registry), git URL, or local path')\n .description('Install an app')\n .action(async (source: string) => {\n const record = await installApp(store, source);\n console.log(`Installed ${record.name}@${record.version} (${record.source.type})`);\n console.log(` data: ${store.appDataDir(record.name)}`);\n console.log(` config: ${store.appConfigPath(record.name)}`);\n await printSetupHints(record.name);\n });\n\n/** After install: what still needs configuring, and which integrations exist. */\nasync function printSetupHints(name: string): Promise<void> {\n let manifest: Awaited<ReturnType<typeof loadApp>>['manifest'];\n try {\n ({ manifest } = await loadApp(store.appRepoDir(name)));\n } catch {\n return; // install already succeeded; hints are best-effort\n }\n const { status } = await resolveAppConfig(store, manifest);\n if (status.missing.length > 0) {\n console.log(` requires: ${status.missing.join(', ')}`);\n }\n const optional = status.integrations.filter((i) => i.optional && !i.configured);\n if (optional.length > 0) {\n const list = optional.map((i) => `${i.name} (needs ${i.missing.join(', ')})`).join(', ');\n console.log(` optional integrations: ${list}`);\n }\n if (status.missing.length > 0 || optional.length > 0) {\n console.log(` configure via the dashboard settings page or the config file above`);\n }\n}\n\nprogram\n .command('update')\n .argument('<name>', 'installed app name')\n .description('Update an installed app')\n .action(async (name: string) => {\n const record = await updateApp(store, name);\n console.log(`Updated ${record.name} to ${record.version}`);\n });\n\nprogram\n .command('remove')\n .argument('<name>', 'installed app name')\n .option('--purge', 'also delete the app data and config')\n .description('Remove an installed app (keeps data unless --purge)')\n .action(async (name: string, options: { purge?: boolean }) => {\n await removeApp(store, name, { purge: options.purge ?? false });\n console.log(options.purge ? `Removed ${name} (purged)` : `Removed ${name} (data kept)`);\n });\n\n/** Prints archive paths the way the user would type them. */\nfunction displayPath(path: string): string {\n const rel = relative(process.cwd(), path);\n return rel === '' || rel.startsWith('..') || isAbsolute(rel) ? path : `./${rel}`;\n}\n\nprogram\n .command('export')\n .argument('<name>', 'installed app name')\n .option('-o, --out <file>', 'archive path (default: ./<name>-<version>.tar.gz)')\n .description('Pack an app container and its chat transcripts into a tar.gz')\n .action(async (name: string, options: { out?: string }) => {\n const result = await exportApp(store, new ClaudeCodeProvider(), name, {\n ...(options.out && { out: options.out }),\n });\n for (const warning of result.warnings) console.log(` warning: ${warning}`);\n console.log(\n `Exported ${result.record.name}@${result.record.version} -> ${displayPath(result.archivePath)} (${result.transcriptCount} transcripts)`,\n );\n if (result.record.source.type === 'local') {\n console.log(' note: repo/ was captured as real files, not as the local symlink');\n }\n });\n\n// `import` is a reserved word, so the handler cannot be named after it; the\n// commander command string itself is fine.\nasync function importArchive(file: string): Promise<void> {\n const result = await importApp(store, new ClaudeCodeProvider(), file);\n for (const warning of result.warnings) console.log(` warning: ${warning}`);\n console.log(\n `Imported ${result.record.name}@${result.record.version} (${result.transcriptCount} transcripts restored)`,\n );\n console.log(` data: ${store.appDataDir(result.record.name)}`);\n console.log(` config: ${store.appConfigPath(result.record.name)}`);\n}\n\nprogram\n .command('import')\n .argument('<file>', 'archive produced by alashi export')\n .description('Restore an app container and its chat transcripts from a tar.gz')\n .action(importArchive);\n\nprogram\n .command('list')\n .option('--json', 'machine-readable output')\n .description('List installed apps')\n .action(async (options: { json?: boolean }) => {\n const usage = new UsageRecorder(store);\n const records = await store.listInstalled();\n const spend = new Map(\n await Promise.all(\n records.map(\n async (record) => [record.name, await budgetStatus(store, usage, record.name)] as const,\n ),\n ),\n );\n if (options.json) {\n console.log(\n JSON.stringify(\n records.map((record) => {\n const budget = spend.get(record.name);\n return {\n ...record,\n todayCostUsd: budget?.spentUsd ?? 0,\n overBudget: budget?.overBudget ?? false,\n ...(budget?.dailyUsd !== undefined && { dailyBudgetUsd: budget.dailyUsd }),\n };\n }),\n null,\n 2,\n ),\n );\n return;\n }\n if (records.length === 0) {\n console.log('No apps installed. Try: alashi install <name|git-url|path>');\n return;\n }\n for (const record of records) {\n const origin = record.source.type === 'git' ? record.source.url : record.source.path;\n const budget = spend.get(record.name);\n const today = (budget?.spentUsd ?? 0).toFixed(4);\n const cap =\n budget?.dailyUsd === undefined\n ? ''\n : ` of $${budget.dailyUsd.toFixed(2)} cap${budget.overBudget ? ' (OVER BUDGET)' : ''}`;\n console.log(\n `${record.name}@${record.version} ${record.source.type} ${origin} $${today} today${cap}`,\n );\n }\n });\n\nprogram\n .command('budget')\n .argument('<name>', 'installed app name')\n .argument('[dailyUsd]', 'daily cap in USD, or \"none\" to remove it (omit to just show)')\n .description(\"Show or set an app's host-enforced daily AI spend cap\")\n .action(async (name: string, dailyUsd?: string) => {\n if (!(await store.readAppRecord(name))) throw new Error(`App \"${name}\" is not installed`);\n if (dailyUsd !== undefined) {\n const cleared = dailyUsd === 'none' || dailyUsd === 'off';\n const amount = cleared ? undefined : Number(dailyUsd);\n if (amount !== undefined && (!Number.isFinite(amount) || amount < 0)) {\n throw new Error(`Invalid daily spend cap: ${dailyUsd}`);\n }\n await setAppBudget(store, name, amount);\n }\n const status = await budgetStatus(store, new UsageRecorder(store), name);\n const spent = `$${status.spentUsd.toFixed(4)} spent on ${status.date} (UTC)`;\n if (status.dailyUsd === undefined) {\n console.log(`${name}: no daily cap — ${spent}`);\n return;\n }\n console.log(\n `${name}: $${status.dailyUsd.toFixed(2)} daily cap — ${spent}, $${(status.remainingUsd ?? 0).toFixed(4)} left`,\n );\n if (status.overBudget)\n console.log(' OVER BUDGET: new AI sessions are refused until 00:00 UTC');\n });\n\nconst registry = program.command('registry').description('Manage app registries');\n\nregistry\n .command('add')\n .argument('<name>', 'registry name')\n .argument('<url>', 'index.json URL or git repo URL')\n .action(async (name: string, url: string) => {\n const index = await addRegistry(store, name, url);\n console.log(`Added registry \"${name}\" (${index.apps.length} apps)`);\n });\n\nregistry\n .command('remove')\n .argument('<name>', 'registry name')\n .action(async (name: string) => {\n await removeRegistry(store, name);\n console.log(`Removed registry \"${name}\"`);\n });\n\nregistry.command('list').action(async () => {\n const config = await store.readGlobalConfig();\n if (config.registries.length === 0) {\n console.log('No registries configured.');\n return;\n }\n for (const r of config.registries) console.log(`${r.name} ${r.url}`);\n});\n\nprogram.parseAsync().catch((err: unknown) => {\n console.error(err instanceof Error ? err.message : err);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
package/dist/index.d.mts CHANGED
@@ -1,12 +1,5 @@
1
- import { AIProvider, AppContext, AppDefinition, AppManifest, ConfigObject } from "@alashi/apps-sdk";
1
+ import { AIProvider, AppContext, AppDefinition, AppManifest, AppSdkError, ConfigObject } from "@alashi/apps-sdk";
2
2
  import { Hono } from "hono";
3
- //#region src/compat.d.ts
4
- /**
5
- * Enforces the manifest's declared `sdkVersion` range against the SDK
6
- * this host is built with. Apps that declare nothing are accepted.
7
- */
8
- declare function checkSdkCompatibility(manifest: AppManifest): void;
9
- //#endregion
10
3
  //#region src/sources.d.ts
11
4
  type AppSourceSpec = {
12
5
  type: "git";
@@ -32,6 +25,14 @@ interface RegistryRef {
32
25
  name: string;
33
26
  url: string;
34
27
  }
28
+ /** Host-enforced spend limits for one app. */
29
+ interface AppBudget {
30
+ /**
31
+ * Daily AI spend ceiling in USD. Once the app's spend for the UTC day
32
+ * reaches it, the host refuses `createSession`. `0` means "no AI at all".
33
+ */
34
+ dailyUsd: number;
35
+ }
35
36
  interface GlobalConfig {
36
37
  registries: RegistryRef[];
37
38
  port?: number;
@@ -39,6 +40,15 @@ interface GlobalConfig {
39
40
  host?: string;
40
41
  /** Per-app global overrides, keyed by app name (middle config layer). */
41
42
  apps?: Record<string, ConfigObject>;
43
+ /**
44
+ * Host-enforced per-app spend caps, keyed by app name.
45
+ *
46
+ * Deliberately a sibling of `apps` rather than a key inside it: everything in
47
+ * `apps` is merged into the app's config and can be overridden by the app's
48
+ * own `config.json` layer, which would let an app raise its own ceiling.
49
+ * A cap is enforcement, so it lives where only the host can set it.
50
+ */
51
+ budgets?: Record<string, AppBudget>;
42
52
  }
43
53
  interface Conversation {
44
54
  id: string;
@@ -64,6 +74,19 @@ interface JobState {
64
74
  lastRun: Record<string, string>;
65
75
  runs: JobRun[];
66
76
  }
77
+ /** One UTC day of AI spend for an app. */
78
+ interface UsageDay {
79
+ /** UTC calendar day, `YYYY-MM-DD`. */
80
+ date: string;
81
+ /** Sum of the `costUsd` the provider reported for that day's turns. */
82
+ costUsd: number;
83
+ /** Turns that reported a cost (turns without one are not counted). */
84
+ turns: number;
85
+ }
86
+ /** Host-side AI spend for an app, oldest day first, trimmed to the retention window. */
87
+ interface UsageState {
88
+ days: UsageDay[];
89
+ }
67
90
  interface InstallRecord {
68
91
  name: string;
69
92
  version: string;
@@ -93,6 +116,7 @@ declare class Store {
93
116
  appRecordPath(name: string): string;
94
117
  appChatStatePath(name: string): string;
95
118
  appJobStatePath(name: string): string;
119
+ appUsagePath(name: string): string;
96
120
  registryCachePath(name: string): string;
97
121
  readGlobalConfig(): Promise<GlobalConfig>;
98
122
  writeGlobalConfig(config: GlobalConfig): Promise<void>;
@@ -104,10 +128,149 @@ declare class Store {
104
128
  writeChatState(name: string, state: ChatState): Promise<void>;
105
129
  readJobState(name: string): Promise<JobState>;
106
130
  writeJobState(name: string, state: JobState): Promise<void>;
131
+ readUsageState(name: string): Promise<UsageState>;
132
+ writeUsageState(name: string, state: UsageState): Promise<void>;
107
133
  listInstalled(): Promise<InstallRecord[]>;
108
134
  }
109
135
  //#endregion
136
+ //#region src/usage.d.ts
137
+ /** Days of history kept in usage.json; older days are dropped on write. */
138
+ declare const USAGE_RETENTION_DAYS = 90;
139
+ /** The UTC calendar day (`YYYY-MM-DD`) a timestamp falls in. */
140
+ declare function utcDay(at?: Date): string;
141
+ /** Money for humans: sub-dollar spend needs the extra digits, larger sums don't. */
142
+ declare function formatUsd(value: number): string;
143
+ /** A summary of an app's recorded spend, as served by GET /api/apps/:name/usage. */
144
+ interface UsageSummary {
145
+ /** The UTC day "today" refers to. */
146
+ date: string;
147
+ /** Spend recorded so far today. */
148
+ todayCostUsd: number;
149
+ /** Turns recorded so far today. */
150
+ todayTurns: number;
151
+ /** Spend across the whole retention window. */
152
+ totalCostUsd: number;
153
+ /** Days kept in the retention window. */
154
+ retentionDays: number;
155
+ /** Daily series, oldest first; days with no spend are absent. */
156
+ days: UsageDay[];
157
+ }
158
+ /**
159
+ * Per-app, per-UTC-day AI spend, persisted as OS bookkeeping in the app
160
+ * container (`apps/<name>/usage.json`, next to jobs.json and chat.json).
161
+ *
162
+ * Cost arrives from the `result` event's `costUsd`, which the SDK contract
163
+ * marks optional: engines that do not report cost simply contribute nothing.
164
+ * Writes are serialized per app so concurrent turns cannot lose an update,
165
+ * and each app has its own queue so a slow write for one app never delays
166
+ * another.
167
+ */
168
+ declare class UsageRecorder {
169
+ private readonly store;
170
+ private readonly retentionDays;
171
+ /** Serializes usage.json read-modify-write per app. */
172
+ private readonly queues;
173
+ constructor(store: Store, retentionDays?: number);
174
+ /**
175
+ * Adds one turn's cost to the app's total for the UTC day of `at`.
176
+ * A missing, non-finite, or negative `costUsd` is ignored (no turn counted,
177
+ * no file written) so a provider that omits cost cannot corrupt the totals.
178
+ */
179
+ record(appName: string, costUsd: number | undefined, at?: Date): Promise<void>;
180
+ /** The app's recorded days, oldest first, with expired days filtered out. */
181
+ read(appName: string, at?: Date): Promise<UsageState>;
182
+ /** Today's spend plus the daily series, for the dashboard and the usage endpoint. */
183
+ summary(appName: string, at?: Date): Promise<UsageSummary>;
184
+ /** Drops days outside the retention window and normalizes ordering/values. */
185
+ private prune;
186
+ private enqueue;
187
+ }
188
+ //#endregion
189
+ //#region src/budget.d.ts
190
+ /**
191
+ * Stable discriminator on the thrown error. Apps see the host's error object
192
+ * across a package boundary (and, once `AppContext` becomes RPC, across a
193
+ * process boundary), so the reliable check is this code — not `instanceof`.
194
+ */
195
+ declare const BUDGET_EXCEEDED_CODE = "budget_exceeded";
196
+ /** Today's spend for an app measured against its cap. */
197
+ interface BudgetStatus {
198
+ /** The UTC calendar day the numbers cover. */
199
+ date: string;
200
+ /** Spend recorded for that day. */
201
+ spentUsd: number;
202
+ /** The configured daily cap; absent when the app has none. */
203
+ dailyUsd?: number;
204
+ /** Spend left before sessions are refused; absent when there is no cap. */
205
+ remainingUsd?: number;
206
+ /** True only when a cap is set and the day's spend has reached it. */
207
+ overBudget: boolean;
208
+ }
209
+ /**
210
+ * Refusal from the `createSession` budget gate.
211
+ *
212
+ * Extends the SDK's `AppSdkError` so it stays inside the taxonomy apps already
213
+ * catch, and carries a `code` plus the numbers behind the decision so an app
214
+ * can degrade gracefully (queue the work, skip the run, tell the user) instead
215
+ * of treating a spend cap as a crash. The message is written to stand alone:
216
+ * it is often the only text a human sees, rendered somewhere far from the host.
217
+ */
218
+ declare class BudgetExceededError extends AppSdkError {
219
+ readonly code: typeof BUDGET_EXCEEDED_CODE;
220
+ readonly appName: string;
221
+ /** UTC day the cap applies to. */
222
+ readonly date: string;
223
+ /** The configured cap in USD. */
224
+ readonly limitUsd: number;
225
+ /** Spend already recorded for `date`. */
226
+ readonly spentUsd: number;
227
+ constructor(appName: string, params: {
228
+ date: string;
229
+ limitUsd: number;
230
+ spentUsd: number;
231
+ });
232
+ /** Duck-typed check that survives duplicate copies of this module. */
233
+ static is(err: unknown): err is BudgetExceededError;
234
+ }
235
+ /**
236
+ * The cap the *host* has set for an app, or undefined when there is none.
237
+ *
238
+ * Read only from `GlobalConfig.budgets`, which is deliberately not part of the
239
+ * config layers merged into `ctx.config`: an app must not be able to raise its
240
+ * own ceiling through its manifest defaults or `apps/<name>/config.json`.
241
+ *
242
+ * A hand-edited cap that is not a finite number >= 0 is ignored rather than
243
+ * treated as zero — a typo in config.json should not brick an app, and the
244
+ * dashboard/settings write path validates before it ever gets stored.
245
+ */
246
+ declare function appBudget(config: GlobalConfig, name: string): number | undefined;
247
+ /** Combines an already-read usage summary with a cap; no I/O. */
248
+ declare function budgetStatusFor(summary: UsageSummary, dailyUsd: number | undefined): BudgetStatus;
249
+ /** Today's spend against the app's cap, for the gate, the dashboard, and settings. */
250
+ declare function budgetStatus(store: Store, usage: UsageRecorder, name: string, at?: Date): Promise<BudgetStatus>;
251
+ /**
252
+ * Fail-closed gate: throws `BudgetExceededError` when the app has already
253
+ * spent its daily cap. Evaluated fresh on every call, so a cap edit or a
254
+ * mid-day overspend takes effect without restarting the app.
255
+ */
256
+ declare function assertWithinBudget(store: Store, usage: UsageRecorder, name: string, at?: Date): Promise<void>;
257
+ /**
258
+ * Sets (or, with `undefined`, clears) an app's daily cap in the host config.
259
+ * Rejects anything that is not a finite, non-negative dollar amount; `0` is a
260
+ * legitimate value meaning "no AI spend at all for this app".
261
+ */
262
+ declare function setAppBudget(store: Store, name: string, dailyUsd: number | undefined): Promise<void>;
263
+ //#endregion
264
+ //#region src/compat.d.ts
265
+ /**
266
+ * Enforces the manifest's declared `sdkVersion` range against the SDK
267
+ * this host is built with. Apps that declare nothing are accepted.
268
+ */
269
+ declare function checkSdkCompatibility(manifest: Pick<AppManifest, "name" | "sdkVersion">): void;
270
+ //#endregion
110
271
  //#region src/installer.d.ts
272
+ /** Lifecycle scripts stay off: installing an app must not execute arbitrary code. */
273
+ declare function installDependencies(dir: string): Promise<void>;
111
274
  interface InstallOptions {
112
275
  /** Expected app name (enforced against the manifest when set). */
113
276
  expectName?: string;
@@ -138,6 +301,50 @@ interface LoadedApp {
138
301
  */
139
302
  declare function loadApp(repoDir: string): Promise<LoadedApp>;
140
303
  //#endregion
304
+ //#region src/portability.d.ts
305
+ /**
306
+ * Container portability. An app is only half-portable on its own: the
307
+ * container holds sessionId pointers, but the conversation transcripts live
308
+ * with the engine (keyed by the app's data dir as cwd). An export therefore
309
+ * snapshots the container *plus* the transcripts, engine-neutrally, and the
310
+ * import re-keys them to this machine's data dir.
311
+ *
312
+ * Archive layout (tar.gz):
313
+ * alashi-export.json { formatVersion, exportedAt, app, transcripts }
314
+ * container/ the contents of $ALASHI_HOME/apps/<name>/
315
+ * transcripts/<sessionId>.jsonl
316
+ */
317
+ declare const EXPORT_FORMAT_VERSION = 1;
318
+ interface ExportMetadata {
319
+ formatVersion: number;
320
+ exportedAt: string;
321
+ app: InstallRecord;
322
+ /** Session ids that have a transcripts/<id>.jsonl entry in the archive. */
323
+ transcripts: string[];
324
+ }
325
+ interface ExportOptions {
326
+ /** Archive path; defaults to ./<name>-<version>.tar.gz under the cwd. */
327
+ out?: string;
328
+ }
329
+ interface ExportResult {
330
+ archivePath: string;
331
+ record: InstallRecord;
332
+ transcriptCount: number;
333
+ warnings: string[];
334
+ }
335
+ interface ImportResult {
336
+ record: InstallRecord;
337
+ transcriptCount: number;
338
+ warnings: string[];
339
+ }
340
+ /**
341
+ * Packs apps/<name>/ plus the transcripts of every conversation that ever had
342
+ * a turn into a single self-contained tar.gz.
343
+ */
344
+ declare function exportApp(store: Store, provider: AIProvider, name: string, options?: ExportOptions): Promise<ExportResult>;
345
+ /** Restores a container exported by {@link exportApp}, plus its transcripts. */
346
+ declare function importApp(store: Store, provider: AIProvider, archivePath: string): Promise<ImportResult>;
347
+ //#endregion
141
348
  //#region src/registry.d.ts
142
349
  interface RegistryApp {
143
350
  name: string;
@@ -175,8 +382,12 @@ declare function resolveApp(store: Store, appName: string): Promise<ResolvedApp>
175
382
  * Setup state shapes the session defaults: unconfigured MCP servers are
176
383
  * dropped (with their tools) so interpolation never hits a missing key, and
177
384
  * while required keys are missing the scoped provider fails closed.
385
+ *
386
+ * When a `usage` recorder is supplied, every session opened through the
387
+ * returned context reports its per-turn cost to it, and the app's host-set
388
+ * daily spend cap (if any) is enforced on every createSession.
178
389
  */
179
- declare function buildAppContext(store: Store, manifest: AppManifest, provider: AIProvider): Promise<AppContext>;
390
+ declare function buildAppContext(store: Store, manifest: AppManifest, provider: AIProvider, usage?: UsageRecorder): Promise<AppContext>;
180
391
  //#endregion
181
392
  //#region src/scheduler.d.ts
182
393
  interface SchedulableApp {
@@ -235,10 +446,17 @@ interface AppServer {
235
446
  * The host HTTP surface:
236
447
  * GET / dashboard of installed apps
237
448
  * GET /settings/:name per-app settings page (config form)
238
- * GET /api/apps installed apps as JSON
449
+ * GET /api/apps installed apps as JSON, each with todayCostUsd
450
+ * and its daily spend cap / over-budget state
451
+ * GET /api/apps/:name/usage recorded AI spend: today's total, a daily series,
452
+ * and the budget status
453
+ * GET /api/apps/:name/budget today's spend against the app's daily cap
454
+ * PUT /api/apps/:name/budget { dailyUsd: number | null } -> set or clear the cap
455
+ * (a host setting; apps cannot set their own)
239
456
  * POST /api/apps/:name/chat { message, conversationId? } -> SSE stream of AgentEvents
240
457
  * (omit conversationId to start a new conversation; the
241
- * id is echoed in the x-conversation-id response header)
458
+ * id is echoed in the x-conversation-id response header;
459
+ * 402 when the app is over its daily spend cap)
242
460
  * GET /api/apps/:name/chat/conversations conversations, most recent first
243
461
  * POST /api/apps/:name/chat/conversations { sessionId?, title? } -> create a conversation,
244
462
  * optionally adopting an existing provider session (e.g. an
@@ -260,5 +478,5 @@ interface AppServer {
260
478
  */
261
479
  declare function createServer(store: Store, provider: AIProvider): AppServer;
262
480
  //#endregion
263
- export { type AppServer, type AppSourceSpec, type GlobalConfig, type InstallOptions, type InstallRecord, type LoadedApp, type RegistryApp, type RegistryIndex, type RegistryRef, type RemoveOptions, type ResolvedApp, type SourceArg, Store, addRegistry, buildAppContext, checkSdkCompatibility, createServer, fetchSource, installApp, loadApp, parseSourceArg, readCachedIndex, refreshRegistry, removeApp, removeRegistry, resolveApp, updateApp, updateSource };
481
+ export { type AppBudget, type AppServer, type AppSourceSpec, BUDGET_EXCEEDED_CODE, BudgetExceededError, type BudgetStatus, EXPORT_FORMAT_VERSION, type ExportMetadata, type ExportOptions, type ExportResult, type GlobalConfig, type ImportResult, type InstallOptions, type InstallRecord, type LoadedApp, type RegistryApp, type RegistryIndex, type RegistryRef, type RemoveOptions, type ResolvedApp, type SourceArg, Store, USAGE_RETENTION_DAYS, type UsageDay, UsageRecorder, type UsageState, type UsageSummary, addRegistry, appBudget, assertWithinBudget, budgetStatus, budgetStatusFor, buildAppContext, checkSdkCompatibility, createServer, exportApp, fetchSource, formatUsd, importApp, installApp, installDependencies, loadApp, parseSourceArg, readCachedIndex, refreshRegistry, removeApp, removeRegistry, resolveApp, setAppBudget, updateApp, updateSource, utcDay };
264
482
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/compat.ts","../src/sources.ts","../src/store.ts","../src/installer.ts","../src/loader.ts","../src/registry.ts","../src/runtime.ts","../src/scheduler.ts","../src/server.ts"],"mappings":";;;;;;;iBAOgB,sBAAsB,UAAU;;;KCCpC;EACN;EAAa;EAAa;;EAC1B;EAAe;;;KAGT,YAAY;EAAkB;EAAkB;;iBAE5C,eAAe,cAAc;;iBAwBvB,YAAY,MAAM,eAAe,kBAAkB;;iBAanD,aAAa,MAAM,eAAe,kBAAkB;;;UC9CzD;EACf;EACA;;UAGe;EACf,YAAY;EACZ;;EAEA;;EAEA,OAAO,eAAe;;UAGP;EACf;;EAEA;EACA;EACA;;;UAIe;EACf,eAAe;;UAGA;EACf;EACA;EACA;EACA;EACA;EACA;;;UAIe;EACf,SAAS;EACT,MAAM;;UAGS;EACf;EACA;;EAEA;EACA,QAAQ;EACR;EACA;;;;;;;cAwBW;WACF;EAET,YAAY;MAIR;MAIA;MAIA;MAIA;;EAKJ,OAAO;EAOP,WAAW;EAIX,WAAW;EAIX,cAAc;EAId,cAAc;EAId,iBAAiB;EAIjB,gBAAgB;EAIhB,kBAAkB;EAOlB,oBAA0B,QAAQ;EAIlC,kBAAwB,QAAQ,eAAe;EAI/C,cAAoB,eAAe,QAAQ;EAI3C,eAAqB,QAAQ,gBAAgB;EAI7C,cAAoB,eAAe,QAAQ;EAI3C,eAAqB,cAAc,QAAQ,eAAe;EAI1D,cAAoB,eAAe,QAAQ;EAK3C,eAAqB,cAAc,OAAO,YAAY;EAItD,aAAmB,eAAe,QAAQ;EAK1C,cAAoB,cAAc,OAAO,WAAW;EAIpD,iBAAuB,QAAQ;;;;UC9IhB;;EAEf;;;;;;;iBAQoB,WACpB,OAAO,OACP,iBAAiB,eACjB,UAAS,iBACR,QAAQ;iBA0EW,UAAU,OAAO,OAAO,eAAe,QAAQ;UAuBpD;;EAEf;;iBAGoB,UACpB,OAAO,OACP,cACA,UAAS,gBACR;;;UCzIc;EACf,YAAY;EACZ,UAAU;EACV;;;;;;;iBAQoB,QAAQ,kBAAkB,QAAQ;;;UC5BvC;EACf;EACA;EACA;EACA;EACA;;UAGe;EACf,MAAM;;;;;;;iBAkCc,gBAAgB,OAAO,OAAO,UAAU,cAAc,QAAQ;iBAuB9D,gBACpB,OAAO,OACP,eACC,QAAQ;iBASW,YAAY,OAAO,OAAO,cAAc,cAAc,QAAQ;iBAW9D,eAAe,OAAO,OAAO,eAAe;UAWjD;EACf;EACA,KAAK;;;iBAIe,WAAW,OAAO,OAAO,kBAAkB,QAAQ;;;;;;;;;;;;;iBC8BnD,gBACpB,OAAO,OACP,UAAU,aACV,UAAU,aACT,QAAQ;;;UC/IM;EACf,UAAU;EACV,KAAK;EACL,cAAc,gBAAgB,KAAK,eAAe;;UAGnC;EACf;EACA;EACA;;EAEA;EACA;EACA;EACA;EACA,YAAY;;;;;;;;cAiBD;mBAQT;mBACA;UARM;UACA;UACA;;UAEA;EAER,YACE,OAAwB,OACxB,SAA0B,iBAAiB,QAAQ;EAGrD,SAAe;EAuCf;;;;;EAUA,IACE,iBACA,iBACA,UAAS,oBACR,QAAQ;;EAmCX,OAAa,kBAAkB,QAAQ;UA4B/B;;;;UCpHO;;EAEf,KAAK;;EAEL,WAAW;;EAEX,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBA8BK,aAAa,OAAO,OAAO,UAAU,aAAa"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/sources.ts","../src/store.ts","../src/usage.ts","../src/budget.ts","../src/compat.ts","../src/installer.ts","../src/loader.ts","../src/portability.ts","../src/registry.ts","../src/runtime.ts","../src/scheduler.ts","../src/server.ts"],"mappings":"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"}
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { _ as checkSdkCompatibility, a as installApp, c as addRegistry, d as removeRegistry, f as resolveApp, g as loadApp, h as updateSource, l as readCachedIndex, m as parseSourceArg, n as createServer, o as removeApp, p as fetchSource, r as buildAppContext, s as updateApp, t as Store, u as refreshRegistry } from "./store-Cxxnjq70.mjs";
2
- export { Store, addRegistry, buildAppContext, checkSdkCompatibility, createServer, fetchSource, installApp, loadApp, parseSourceArg, readCachedIndex, refreshRegistry, removeApp, removeRegistry, resolveApp, updateApp, updateSource };
1
+ import { A as UsageRecorder, C as BudgetExceededError, D as budgetStatusFor, E as budgetStatus, M as utcDay, O as setAppBudget, S as BUDGET_EXCEEDED_CODE, T as assertWithinBudget, _ as fetchSource, a as EXPORT_FORMAT_VERSION, b as loadApp, c as installApp, d as updateApp, f as addRegistry, g as resolveApp, h as removeRegistry, j as formatUsd, k as USAGE_RETENTION_DAYS, l as installDependencies, m as refreshRegistry, n as createServer, o as exportApp, p as readCachedIndex, r as buildAppContext, s as importApp, t as Store, u as removeApp, v as parseSourceArg, w as appBudget, x as checkSdkCompatibility, y as updateSource } from "./store-VoJe_U1Q.mjs";
2
+ export { BUDGET_EXCEEDED_CODE, BudgetExceededError, EXPORT_FORMAT_VERSION, Store, USAGE_RETENTION_DAYS, UsageRecorder, addRegistry, appBudget, assertWithinBudget, budgetStatus, budgetStatusFor, buildAppContext, checkSdkCompatibility, createServer, exportApp, fetchSource, formatUsd, importApp, installApp, installDependencies, loadApp, parseSourceArg, readCachedIndex, refreshRegistry, removeApp, removeRegistry, resolveApp, setAppBudget, updateApp, updateSource, utcDay };