From d6356edc0ed4ef44f0d10823ace552c24d662d0d Mon Sep 17 00:00:00 2001
From: Michael McRae
Date: Mon, 3 Aug 2026 11:15:30 +1000
Subject: [PATCH 01/23] 1st commit
---
packages/plugins/.bin/check-branch-diff.sh | 40 ++++++++++++++++++++
packages/plugins/.bin/check-versions.sh | 43 ++++++++++++++++++++++
2 files changed, 83 insertions(+)
create mode 100755 packages/plugins/.bin/check-branch-diff.sh
create mode 100755 packages/plugins/.bin/check-versions.sh
diff --git a/packages/plugins/.bin/check-branch-diff.sh b/packages/plugins/.bin/check-branch-diff.sh
new file mode 100755
index 00000000..e92e82c8
--- /dev/null
+++ b/packages/plugins/.bin/check-branch-diff.sh
@@ -0,0 +1,40 @@
+#!/usr/bin/env bash
+# Compare plugin files in current branch against main branch to verify version bumps
+
+set -e
+
+MAIN_BRANCH="${1:-main}"
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+
+printf "%-25s | %-13s | %-14s | %-14s | %-30s\n" "Plugin Package" "Files Changed" "Main Version" "Branch Version" "Version Bump Status"
+printf "%-25s-+-%-13s-+-%-14s-+-%-14s-+-%-30s\n" "-------------------------" "-------------" "--------------" "--------------" "------------------------------"
+
+for plugin_dir in "${REPO_ROOT}/packages/plugins"/*; do
+ if [ -d "${plugin_dir}" ] && [ -f "${plugin_dir}/package.json" ]; then
+ plugin_name=$(basename "${plugin_dir}")
+ rel_path="packages/plugins/${plugin_name}"
+
+ # Count changed files in this plugin (excluding package.json)
+ changed_count=$(git diff --name-only "${MAIN_BRANCH}...HEAD" -- "${rel_path}" 2>/dev/null | grep -v "package.json" | wc -l || echo "0")
+
+ # Read version from main branch
+ main_version=$(git show "${MAIN_BRANCH}:${rel_path}/package.json" 2>/dev/null | node --input-type=module -e "import fs from 'fs'; console.log(JSON.parse(fs.readFileSync(process.stdin.fd, 'utf8')).version)" 2>/dev/null || echo "[NEW]")
+
+ # Read version from current working branch
+ branch_version=$(node --input-type=module -e "import fs from 'fs'; console.log(JSON.parse(fs.readFileSync(process.argv[2], 'utf8')).version)" dummy "${plugin_dir}/package.json" 2>/dev/null || echo "unknown")
+
+ # Determine status
+ status="Clean (Unchanged)"
+ if [ "${main_version}" = "[NEW]" ]; then
+ status="🆕 New Plugin (v${branch_version})"
+ elif [ "${changed_count}" -gt 0 ]; then
+ if [ "${main_version}" = "${branch_version}" ]; then
+ status="🚨 MODIFIED WITHOUT VERSION BUMP!"
+ else
+ status="✅ Bumped (v${main_version} -> v${branch_version})"
+ fi
+ fi
+
+ printf "%-25s | %-13s | %-14s | %-14s | %-30s\n" "${plugin_name}" "${changed_count}" "${main_version}" "${branch_version}" "${status}"
+ fi
+done
diff --git a/packages/plugins/.bin/check-versions.sh b/packages/plugins/.bin/check-versions.sh
new file mode 100755
index 00000000..05747642
--- /dev/null
+++ b/packages/plugins/.bin/check-versions.sh
@@ -0,0 +1,43 @@
+#!/usr/bin/env bash
+# Check published NPM versions vs local workspace versions for Tempo and plugins
+
+set -e
+
+packages=(
+ "tempo:packages/tempo/package.json"
+ "tempo-plugin-ai:packages/plugins/ai/package.json"
+ "tempo-plugin-astro:packages/plugins/astro/package.json"
+ "tempo-plugin-batch:packages/plugins/batch/package.json"
+ "tempo-plugin-finance:packages/plugins/finance/package.json"
+ "tempo-plugin-snap:packages/plugins/snap/package.json"
+ "tempo-plugin-sync:packages/plugins/sync/package.json"
+)
+
+# Resolve repository root path (3 levels up from packages/plugins/.bin)
+REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
+
+printf "%-38s | %-16s | %-16s | %-12s\n" "Package Name" "Published (NPM)" "Local Workspace" "Status"
+printf "%-38s-+-%-16s-+-%-16s-+-%-12s\n" "--------------------------------------" "----------------" "----------------" "------------"
+
+for entry in "${packages[@]}"; do
+ pkg_name="${entry%%:*}"
+ rel_path="${entry#*:}"
+ full_npm_name="@magmacomputing/${pkg_name}"
+
+ # Fetch published version from NPM registry
+ published_ver=$(npm view "${full_npm_name}" version 2>/dev/null || echo "not published")
+
+ # Read local version from package.json
+ local_ver="unknown"
+ target_json="${REPO_ROOT}/${rel_path}"
+ if [ -f "${target_json}" ]; then
+ local_ver=$(node --input-type=module -e "import fs from 'fs'; console.log(JSON.parse(fs.readFileSync(process.argv[2], 'utf8')).version)" dummy "${target_json}" 2>/dev/null || echo "unknown")
+ fi
+
+ status="Up to date"
+ if [ "${published_ver}" != "${local_ver}" ]; then
+ status="Out of sync"
+ fi
+
+ printf "%-38s | %-16s | %-16s | %-12s\n" "${full_npm_name}" "${published_ver}" "${local_ver}" "${status}"
+done
From 9cfdaae789c41247dbd6409831ded5310244f06d Mon Sep 17 00:00:00 2001
From: Michael McRae
Date: Mon, 3 Aug 2026 16:37:43 +1000
Subject: [PATCH 02/23] catch-up
---
package.json | 2 +-
packages/library/package.json | 2 +-
packages/tempo/CHANGELOG.md | 5 +++++
packages/tempo/package.json | 2 +-
packages/tempo/src/support/support.cache.ts | 12 ++++++++++++
packages/tempo/src/tempo.version.ts | 2 +-
packages/tempo/test/support/cache.test.ts | 14 +++++++++++++-
7 files changed, 34 insertions(+), 5 deletions(-)
diff --git a/package.json b/package.json
index a960cd8a..90a1a6f5 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "tempo-monorepo",
- "version": "3.11.0",
+ "version": "3.11.1",
"private": true,
"engines": {
"node": ">=20.0.0"
diff --git a/packages/library/package.json b/packages/library/package.json
index 312b3090..0e3ec108 100644
--- a/packages/library/package.json
+++ b/packages/library/package.json
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/library",
- "version": "3.11.0",
+ "version": "3.11.1",
"description": "Shared utility library for Tempo",
"author": "Magma Computing Solutions",
"license": "MIT",
diff --git a/packages/tempo/CHANGELOG.md b/packages/tempo/CHANGELOG.md
index a5c684d4..ba4699cb 100644
--- a/packages/tempo/CHANGELOG.md
+++ b/packages/tempo/CHANGELOG.md
@@ -6,6 +6,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
+## [3.11.1] - 2026-08-03
+
+### Added
+- **Cache Serialization (`toJSON`)**: Added native `toJSON()` serialization support to `BoundedCache` and the `Tempo.cache` facade object. Calling `Tempo.cache.toJSON()` or `JSON.stringify(Tempo.cache)` now cleanly converts active, non-expired in-memory cache entries into a plain key-value JavaScript object.
+
## [3.11.0] - 2026-07-31
### Added
diff --git a/packages/tempo/package.json b/packages/tempo/package.json
index 03168496..9b53b523 100644
--- a/packages/tempo/package.json
+++ b/packages/tempo/package.json
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo",
- "version": "3.11.0",
+ "version": "3.11.1",
"engines": {
"node": ">=20.0.0"
},
diff --git a/packages/tempo/src/support/support.cache.ts b/packages/tempo/src/support/support.cache.ts
index d863fa1c..72b4c9a4 100644
--- a/packages/tempo/src/support/support.cache.ts
+++ b/packages/tempo/src/support/support.cache.ts
@@ -186,6 +186,14 @@ export class BoundedCache extends Map {
return super[Symbol.iterator]();
}
+ /**
+ * Returns a plain key-value object of all active non-expired cache entries.
+ */
+ toJSON(): Record {
+ this.evictExpired();
+ return Object.fromEntries(this.entries()) as Record;
+ }
+
static fromEntries(entries: Iterable, maxSize = 1000, ttl = 24 * 60 * 60 * 1000): BoundedCache {
const cache = new BoundedCache(maxSize, ttl);
for (const [k, v] of entries) {
@@ -238,6 +246,10 @@ export function createCacheFacade(getState: () => t.Internal.State) {
getState().cache.set(normalized, String(v));
}
return this;
+ },
+ toJSON() {
+ return getState().cache.toJSON();
}
});
}
+
diff --git a/packages/tempo/src/tempo.version.ts b/packages/tempo/src/tempo.version.ts
index 89eb178b..04e3729c 100644
--- a/packages/tempo/src/tempo.version.ts
+++ b/packages/tempo/src/tempo.version.ts
@@ -5,4 +5,4 @@
* ⚠️ This file is auto-updated by `npm run build:version` (see `bin/update-version.mjs`).
* Do NOT edit manually — your changes will be overwritten on the next build.
*/
-export const TEMPO_VERSION = '3.11.0';
+export const TEMPO_VERSION = '3.11.1';
diff --git a/packages/tempo/test/support/cache.test.ts b/packages/tempo/test/support/cache.test.ts
index 2f2c7f46..e47c4979 100644
--- a/packages/tempo/test/support/cache.test.ts
+++ b/packages/tempo/test/support/cache.test.ts
@@ -75,6 +75,14 @@ describe('Tempo Core Caching Architecture', () => {
expect(cache.has('k2')).toBe(false);
expect(cache.has('k3')).toBe(true);
});
+ it('should serialize to JSON object via toJSON()', () => {
+ const cache = new BoundedCache(10, 10000);
+ cache.set('k1', 'v1');
+ cache.set('k2', 'v2');
+
+ expect(cache.toJSON()).toEqual({ k1: 'v1', k2: 'v2' });
+ expect(JSON.stringify(cache)).toBe('{"k1":"v1","k2":"v2"}');
+ });
});
describe('Tempo.CACHE Enum & Facade', () => {
@@ -84,15 +92,19 @@ describe('Tempo Core Caching Architecture', () => {
expect(Tempo.CACHE.Refresh).toBe('refresh');
});
- it('should expose normalized Tempo.cache facade methods', () => {
+ it('should expose normalized Tempo.cache facade methods and serialize via toJSON()', () => {
Tempo.cache.set(' MY_TERM ', '2026-05-10');
expect(Tempo.cache.has('my_term')).toBe(true);
expect(Tempo.cache.get('my_term')).toBe('2026-05-10');
+ expect(Tempo.cache.toJSON()).toEqual({ my_term: '2026-05-10' });
+ expect(JSON.stringify(Tempo.cache)).toBe('{"my_term":"2026-05-10"}');
+
Tempo.cache.delete('MY_TERM');
expect(Tempo.cache.has('my_term')).toBe(false);
});
+
it('should resolve static glossary terms instantly and record glossary source in parse result', () => {
Tempo.cache.setStatic('eoy_party', '2026-12-31T18:00:00');
From 99697ea2feebe11700e2423bc5b34bf6f3256c91 Mon Sep 17 00:00:00 2001
From: Michael McRae
Date: Tue, 4 Aug 2026 11:27:19 +1000
Subject: [PATCH 03/23] feat(docs): implement TypeDoc Compiler API type
expansion pipeline and add Tempo Library logo
---
packages/library/README.md | 2 +-
packages/library/img/library-logo.svg | 21 ++++
packages/tempo/bin/expand-typedoc.mjs | 114 ++++++++++++++++++
.../doc/6-utility-library/tempo.library.md | 2 +
packages/tempo/img/library-logo.svg | 21 ++++
packages/tempo/package.json | 2 +-
packages/tempo/public/library-logo.svg | 21 ++++
7 files changed, 181 insertions(+), 2 deletions(-)
create mode 100644 packages/library/img/library-logo.svg
create mode 100644 packages/tempo/bin/expand-typedoc.mjs
create mode 100644 packages/tempo/img/library-logo.svg
create mode 100644 packages/tempo/public/library-logo.svg
diff --git a/packages/library/README.md b/packages/library/README.md
index 57fb95a3..27f28657 100644
--- a/packages/library/README.md
+++ b/packages/library/README.md
@@ -1,4 +1,4 @@
-# Magma Library (Internal Reference)
+# Magma Library (Internal Reference)
> [!NOTE]
> **Internal Reference Package**: `packages/library` is an internal monorepo utility suite used across Tempo packages. It is **not** published as a standalone package on npm, and is provided in the documentation as a reference guide for internal architectural utilities and shared routines.
diff --git a/packages/library/img/library-logo.svg b/packages/library/img/library-logo.svg
new file mode 100644
index 00000000..55fb6bdf
--- /dev/null
+++ b/packages/library/img/library-logo.svg
@@ -0,0 +1,21 @@
+
+
+
diff --git a/packages/tempo/bin/expand-typedoc.mjs b/packages/tempo/bin/expand-typedoc.mjs
new file mode 100644
index 00000000..2e5d92a6
--- /dev/null
+++ b/packages/tempo/bin/expand-typedoc.mjs
@@ -0,0 +1,114 @@
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import ts from 'typescript';
+
+const __filename = fileURLToPath(import.meta.url);
+const tempoDir = path.dirname(path.dirname(__filename));
+const libraryDir = path.resolve(tempoDir, '../library');
+const htmlOutputDir = path.resolve(tempoDir, 'public/api/library/types');
+
+console.log('🔍 Running Phase 3: TypeDoc Compiler API Type Expansion Post-Processor...');
+
+// 1. Load TypeScript program for @magmacomputing/library
+const entryPoints = [
+ path.resolve(libraryDir, 'src/common.index.ts'),
+ path.resolve(libraryDir, 'src/browser.index.ts'),
+ path.resolve(libraryDir, 'src/server.index.ts')
+];
+
+const tsconfigPath = path.resolve(libraryDir, 'tsconfig.json');
+const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
+const parsedCmd = ts.parseJsonConfigFileContent(configFile.config, ts.sys, libraryDir);
+
+const program = ts.createProgram(entryPoints, parsedCmd.options);
+const checker = program.getTypeChecker();
+
+// 2. Map of typeName -> expanded type declaration string
+const typeMap = new Map();
+
+for (const sourceFile of program.getSourceFiles()) {
+ if (sourceFile.isDeclarationFile) continue;
+
+ ts.forEachChild(sourceFile, (node) => {
+ if (ts.isTypeAliasDeclaration(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
+ const typeName = node.name.text;
+ const rhsNode = node.type;
+ const rhsType = checker.getTypeAtLocation(rhsNode);
+
+ let expanded = checker.typeToString(
+ rhsType,
+ rhsNode,
+ ts.TypeFormatFlags.NoTruncation |
+ ts.TypeFormatFlags.InTypeAlias |
+ ts.TypeFormatFlags.AllowUniqueESSymbolType
+ );
+
+ // Fallback to node.type.getText() if typeToString returns the typeAlias identifier itself
+ if (expanded === typeName || expanded.startsWith(`${typeName}<`)) {
+ expanded = rhsNode.getText();
+ }
+
+ const typeParams = node.typeParameters?.map(tp => tp.name.text).join(', ');
+ const fullSignature = typeParams ? `${typeName}<${typeParams}> = ${expanded}` : `${typeName} = ${expanded}`;
+
+ typeMap.set(typeName, fullSignature);
+ }
+ });
+}
+
+console.log(`Found ${typeMap.size} exported type aliases from @magmacomputing/library source.`);
+
+// 3. Scan generated HTML files in public/api/library/types/
+if (!fs.existsSync(htmlOutputDir)) {
+ console.error(`❌ Output directory ${htmlOutputDir} does not exist. Run TypeDoc first.`);
+ process.exit(1);
+}
+
+const htmlFiles = fs.readdirSync(htmlOutputDir).filter(f => f.endsWith('.html'));
+let processedCount = 0;
+
+for (const file of htmlFiles) {
+ const filePath = path.join(htmlOutputDir, file);
+ let html = fs.readFileSync(filePath, 'utf-8');
+
+ // Extract the type alias name from filename or page title (e.g. common.index.CountOf.html -> CountOf)
+ const match = file.match(/common\.index\.([A-Za-z0-9_$]+)\.html$/);
+ if (!match) continue;
+
+ const typeName = match[1];
+ const expandedSig = typeMap.get(typeName);
+
+ if (expandedSig) {
+ const injectionHtml = `
+
+
+ 🔍 Expanded Type Evaluation (Compiler API)
+
+
+
type ${escapeHtml(expandedSig)}
+
+
+`;
+
+ // Inject directly after
...
+ const signatureEndIdx = html.indexOf('', html.indexOf('class="tsd-signature"'));
+ if (signatureEndIdx !== -1) {
+ const insertPos = signatureEndIdx + 6;
+ html = html.slice(0, insertPos) + injectionHtml + html.slice(insertPos);
+ fs.writeFileSync(filePath, html, 'utf-8');
+ processedCount++;
+ }
+ }
+}
+
+console.log(`✅ Injected expanded type definitions into ${processedCount} HTML pages in public/api/library/types/`);
+
+function escapeHtml(str) {
+ return str
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+}
diff --git a/packages/tempo/doc/6-utility-library/tempo.library.md b/packages/tempo/doc/6-utility-library/tempo.library.md
index 86bf57e3..481f2a67 100644
--- a/packages/tempo/doc/6-utility-library/tempo.library.md
+++ b/packages/tempo/doc/6-utility-library/tempo.library.md
@@ -1,3 +1,5 @@
+
+
# Tempo Library Functionality
While Tempo is primarily a Date-Time engine, it relies on several custom utilities under the hood to handle data structures, deep cloning, and serialization safely.
diff --git a/packages/tempo/img/library-logo.svg b/packages/tempo/img/library-logo.svg
new file mode 100644
index 00000000..55fb6bdf
--- /dev/null
+++ b/packages/tempo/img/library-logo.svg
@@ -0,0 +1,21 @@
+
+
+
diff --git a/packages/tempo/package.json b/packages/tempo/package.json
index 9b53b523..c0560653 100644
--- a/packages/tempo/package.json
+++ b/packages/tempo/package.json
@@ -234,7 +234,7 @@
"prebuild": "npm run build:version",
"clean": "magma-cli rm dist && (node ../../node_modules/typescript-7/bin/tsc -b --clean || true)",
"prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && if [ -z \"$TEMPO_LICENSE_PATH\" ] || [ ! -f \"$TEMPO_LICENSE_PATH\" ]; then echo '🚨 ERROR: TEMPO_LICENSE_PATH is missing or invalid. Cannot publish Premium build.'; exit 1; fi && npm run build",
- "docs:api": "typedoc && typedoc --options typedoc.library.json",
+ "docs:api": "typedoc && typedoc --options typedoc.library.json && node bin/expand-typedoc.mjs",
"docs:dev": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && vitepress dev",
"docs:build": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && vitepress build",
"docs:preview": "vitepress preview",
diff --git a/packages/tempo/public/library-logo.svg b/packages/tempo/public/library-logo.svg
new file mode 100644
index 00000000..55fb6bdf
--- /dev/null
+++ b/packages/tempo/public/library-logo.svg
@@ -0,0 +1,21 @@
+
+
+
From b5863c89538346907eda879b5be9ae495e1c36bb Mon Sep 17 00:00:00 2001
From: Michael McRae
Date: Wed, 5 Aug 2026 14:32:16 +1000
Subject: [PATCH 04/23] AI API
---
.github/workflows/deploy-docs.yml | 8 +
packages/library/README.md | 2 +-
packages/library/img/library-logo.svg | 43 +-
packages/plugins/ai/CHANGELOG.md | 15 +
packages/plugins/ai/README.md | 52 +-
packages/plugins/ai/doc/architecture.md | 16 +-
packages/plugins/ai/doc/index.md | 17 +
packages/plugins/ai/doc/rate-limits.md | 65 +-
packages/plugins/ai/package.json | 2 +-
packages/plugins/ai/plan/v0.3.0-roadmap.md | 90 +-
packages/plugins/ai/src/core/init.ts | 41 +-
packages/plugins/ai/src/core/manifest.ts | 109 +
packages/plugins/ai/src/core/support.ts | 5 +-
packages/plugins/ai/src/core/types.ts | 35 +
packages/plugins/ai/src/functions/parse.ts | 62 +-
packages/plugins/ai/src/index.ts | 1 +
packages/plugins/ai/test/cache.test.ts | 139 +
packages/plugins/ai/test/index.spec.ts | 26 +-
packages/plugins/ai/test/manifest.test.ts | 142 +
packages/tempo/.vitepress/config.ts | 1 +
.../tempo/.vitepress/theme/data/catalog.json | 2 +-
packages/tempo/CHANGELOG.md | 3 +
packages/tempo/bin/expand-typedoc.mjs | 119 +-
packages/tempo/bin/generate-llms-txt.mjs | 42 +
.../doc/1-getting-started/ai-integration.md | 74 +
.../doc/1-getting-started/installation.md | 11 +
.../doc/3-extending-tempo/tempo.layout.md | 13 +
packages/tempo/img/library-logo.svg | 43 +-
packages/tempo/package.json | 4 +-
packages/tempo/public/bundle.index.html | 1 +
packages/tempo/public/esm_core.index.html | 1 +
packages/tempo/public/esm_full.index.html | 1 +
packages/tempo/public/esm_sh.index.html | 255 +
packages/tempo/public/library-logo.svg | 43 +-
packages/tempo/public/llms-full.txt | 7822 +++++++++++++++++
packages/tempo/public/llms.txt | 74 +
packages/tempo/public/providers.v1.json | 26 +
packages/tempo/public/script.index.html | 1 +
38 files changed, 9180 insertions(+), 226 deletions(-)
create mode 100644 packages/plugins/ai/src/core/manifest.ts
create mode 100644 packages/plugins/ai/test/cache.test.ts
create mode 100644 packages/plugins/ai/test/manifest.test.ts
create mode 100644 packages/tempo/bin/generate-llms-txt.mjs
create mode 100644 packages/tempo/doc/1-getting-started/ai-integration.md
create mode 100644 packages/tempo/public/esm_sh.index.html
create mode 100644 packages/tempo/public/llms-full.txt
create mode 100644 packages/tempo/public/llms.txt
create mode 100644 packages/tempo/public/providers.v1.json
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
index 9a32cd7e..9229287b 100644
--- a/.github/workflows/deploy-docs.yml
+++ b/.github/workflows/deploy-docs.yml
@@ -77,3 +77,11 @@ jobs:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4
+
+ - name: Trigger tempo-workspace AI Context Deploy
+ uses: peter-evans/repository-dispatch@v3
+ with:
+ token: ${{ secrets.TEMPO_WORKSPACE_DISPATCH_TOKEN }}
+ repository: magmacomputing/tempo-workspace
+ event-type: sync-ai-context
+
diff --git a/packages/library/README.md b/packages/library/README.md
index 27f28657..a7fe02a6 100644
--- a/packages/library/README.md
+++ b/packages/library/README.md
@@ -1,4 +1,4 @@
-# Magma Library (Internal Reference)
+# Magma Library (Internal Reference)
> [!NOTE]
> **Internal Reference Package**: `packages/library` is an internal monorepo utility suite used across Tempo packages. It is **not** published as a standalone package on npm, and is provided in the documentation as a reference guide for internal architectural utilities and shared routines.
diff --git a/packages/library/img/library-logo.svg b/packages/library/img/library-logo.svg
index 55fb6bdf..ec7fd7c4 100644
--- a/packages/library/img/library-logo.svg
+++ b/packages/library/img/library-logo.svg
@@ -1,21 +1,34 @@
-
-Tempo community plugin for LLM-powered natural language parsing.
+> **Tempo community plugin for LLM-powered natural language parsing.**
-This plugin bridges the gap between deterministic date-math and unstructured NLP inputs, utilizing large language models (like Gemini, Groq, or OpenAI) to safely and asynchronously parse complex natural language expressions into `Tempo` instances.
+This plugin bridges deterministic date math and unstructured NLP inputs, leveraging LLMs (Gemini, Groq, OpenAI, Ollama) to asynchronously parse complex natural language expressions into type-safe `Tempo` instances.
-> **CRITICAL SECURITY WARNING**: Raw LLM API keys must **never** be exposed in a client-side browser bundle or stored in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). Client-side storage is vulnerable to XSS attacks, malicious scripts, and browser extension extraction, which can result in API key theft and quota abuse. BYOK (Bring Your Own Key) is only secure on backend servers (Node, edge workers). For public frontend applications, you must route requests through a secure backend proxy service.
->
-> **LLM Output Disclaimer**: Large Language Models are probabilistic text generators, not deterministic calculators. Magma Computing Solutions and Tempo core maintainers provide `@magmacomputing/tempo-plugin-ai` "as-is". Developers and organizations are solely responsible for validating AI-generated date and time outputs before relying on them in financial, legal, medical, or time-critical production systems.
+> 🔒 **Security Notice**: Raw LLM API keys must **never** be exposed in client-side browser bundles or client storage (`localStorage`, `sessionStorage`, `IndexedDB`). BYOK is only safe on backend servers or edge runtime proxies.
-## Installation
+---
+
+## ⚡ Quick Start
+
+### 📦 Installation
```bash
npm install @magmacomputing/tempo-plugin-ai
```
-## Setup & Usage
+### 🎯 Usage
```typescript
import { parseAI, initAI, clearAiCache } from '@magmacomputing/tempo-plugin-ai';
-// Initialize with your BYOK API Key (ensuring non-undefined string key)
+// Initialize with your BYOK API keys
initAI({
providers: [
- ...(process.env.GROQ_API_KEY ? [{ id: 'groq', key: process.env.GROQ_API_KEY }] : []),
+ { id: 'groq', key: process.env.GROQ_API_KEY! }
]
});
-// Parse a complex natural language string!
+// Parse natural language into a Tempo instance!
const dt = await parseAI("The penultimate Tuesday before Thanksgiving in 2026");
-// Evict bad parses from the cache
+console.log(dt.format('{yyyy}-{mm}-{dd}')); // 2026-11-17
+console.log(dt.ai?.confidence); // 0.98
+console.log(dt.ai?.provider); // 'groq'
+
+// Evict cached resolution
clearAiCache("The penultimate Tuesday before Thanksgiving in 2026");
```
-Full documentation is available at [https://magmacomputing.github.io/magma/doc/9-plugins/ai.index.html](https://magmacomputing.github.io/magma/doc/9-plugins/ai.index.html).
+---
+
+## ✨ Features & Architecture
+
+* 🤖 **Multi-Provider Routing**: Native support for Groq, OpenAI, Gemini, Mistral, and local Ollama nodes with automatic fallback.
+* 🌐 **Dynamic Provider Manifest**: Model IDs and endpoints are lazily updated via hosted JSON manifests with 1500ms fail-open air-gapped fallbacks.
+* ⚡ **Two-Tier Caching**: Combines fast local in-memory LRU caching (`BoundedCache`) with optional async storage adapters (`AiCacheAdapter` for Redis / Cloudflare KV).
+* ⏱️ **Cascading TTL Policies**: Granular TTL control at call-site, provider, or global levels.
+* 🛡️ **Fail-Safe Confidence Bounds**: Configurable `minConfidence` thresholds and array batch processing with soft-error handling.
+
+---
+
+## 📚 Documentation
+
+For complete API references, architecture guides, and advanced examples (Redis adapters, custom provider setups, race/consensus execution modes):
+
+📖 **[Read the Official AI Plugin Documentation](https://magmacomputing.github.io/magma/doc/9-plugins/ai.index.html)**
+
+---
-## Licensing
+## ⚖️ Licensing
-This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required.
+This is a **Community** plugin. It is completely free and open-source for personal and commercial use under the MIT license.
diff --git a/packages/plugins/ai/doc/architecture.md b/packages/plugins/ai/doc/architecture.md
index 7a176adb..ab978bce 100644
--- a/packages/plugins/ai/doc/architecture.md
+++ b/packages/plugins/ai/doc/architecture.md
@@ -40,12 +40,26 @@ initAI({
id: 'local',
key: 'no-key-needed',
url: 'http://localhost:11434/v1/chat/completions',
- model: 'your-local-model'
+ model: 'your-local-model',
+ options: { timeout: 5000 } // Custom provider-level timeout (5s)
}
]
});
```
+### Dynamic Provider Manifests & Air-Gapped Fallback
+
+By default, `@magmacomputing/tempo-plugin-ai` lazily fetches provider defaults (model IDs, endpoints, token parameter keys) from `https://tempo.magmacomputing.com.au/providers.v1.json` once per application lifecycle.
+
+- **Fail-Open & Air-Gapped Fallback**: If the network request fails, times out (1500ms limit), or the application is running offline or in an air-gapped environment, `initAI()` automatically and silently falls back to compiled local defaults (`DEFAULT_PROVIDERS`).
+- **Disabling Remote Manifest**: Pass `remoteConfigUrl: false` to disable remote manifest fetching entirely:
+ ```typescript
+ initAI({
+ providers: [{ id: 'groq', key: process.env.GROQ_API_KEY! }],
+ remoteConfigUrl: false // Disable remote manifest fetching
+ });
+ ```
+
### Frontend Security Warning
> [!CAUTION]
> **Never** expose a raw LLM API key in a client-side browser bundle (like React or Vue) or store it in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). Any Cross-Site Scripting (XSS) vulnerability, compromised NPM package, or malicious browser extension can easily inspect client-side storage and steal secret keys, leading to quota exhaustion, billing fraud, or permanent provider bans. BYOK keys are *only* safe on backend servers or edge workers.
diff --git a/packages/plugins/ai/doc/index.md b/packages/plugins/ai/doc/index.md
index 644001bd..89b252db 100644
--- a/packages/plugins/ai/doc/index.md
+++ b/packages/plugins/ai/doc/index.md
@@ -82,6 +82,23 @@ const agreed = await parseAI("The penultimate Tuesday before Thanksgiving", {
});
```
+## Timeout Controls & SLAs
+
+Prevent hanging requests using the 3-tier timeout hierarchy (`call-site` > `provider` > `global` > `default 15s`):
+
+```typescript
+// Global timeout across all AI requests
+initAI({
+ providers: [
+ { id: 'groq', key: process.env.GROQ_API_KEY, options: { timeout: 2000 } } // 2s timeout for fast provider
+ ],
+ timeout: 5000 // 5s global default timeout
+});
+
+// Hard 3-second SLA override for a specific call-site
+const dt = await parseAI("Next Tuesday at 4pm", { timeout: 3000 });
+```
+
## Debugging & Forced Evaluation
When building your LLM queries, it is often useful to see exactly how AI functions route your data.
diff --git a/packages/plugins/ai/doc/rate-limits.md b/packages/plugins/ai/doc/rate-limits.md
index 1765ec3e..50252aac 100644
--- a/packages/plugins/ai/doc/rate-limits.md
+++ b/packages/plugins/ai/doc/rate-limits.md
@@ -5,18 +5,35 @@ When using third-party AI APIs, your application is subject to strict rate limit
The plugin automatically tracks these limits by reading the standard `x-ratelimit-*` HTTP headers returned by providers like OpenAI and Groq.
## Tracking Quota Real-time
-To expose this data without ruining the clean return signatures of Tempo AI functions, the plugin provides a dedicated utility function: `getAiRateLimits()`.
+
+Quota and rate-limit metadata can be inspected in two convenient ways:
+
+### 1. Request-Locked Instance Metadata (`dt.ai.limits`)
+Every `Tempo` instance returned by `parseAI` includes a frozen `.ai.limits` snapshot representing the exact rate limit state returned by the provider HTTP headers for *that specific request*:
+
+```typescript
+const dt = await parseAI("The third Friday of next month");
+
+if (dt.ai?.limits) {
+ console.log(`Remaining Tokens: ${dt.ai.limits.remainingTokens}`);
+ console.log(`Remaining Requests: ${dt.ai.limits.remainingRequests}`);
+ console.log(`Resets At: ${dt.ai.limits.resetAt?.format('{hh}:{mi}:{ss}')}`);
+}
+```
+
+### 2. Global State Utility (`getAiRateLimits()`)
+For quick status checks or global monitoring across the application lifecycle, `getAiRateLimits()` exposes the stats from the most recent LLM request:
```typescript
import { getAiRateLimits } from '@magmacomputing/tempo-plugin-ai';
-// Returns the stats from the most recent LLM proxy request
+// Returns global stats from the most recent LLM proxy request
const stats = getAiRateLimits();
if (stats) {
console.log(`Remaining Tokens: ${stats.remainingTokens}`);
console.log(`Remaining Requests: ${stats.remainingRequests}`);
- console.log(`Limits Reset At: ${stats.resetAt.format('{hh}:{mi}:{ss}')}`);
+ console.log(`Limits Reset At: ${stats.resetAt?.format('{hh}:{mi}:{ss}')}`);
}
```
@@ -108,15 +125,45 @@ If you want to explicitly query the LLM again and *overwrite* the existing cache
const dt = await parseAI("Q3_START", { force: true });
```
-### Extensible Caching (Enterprise)
-For edge environments or custom application architectures, you can provide custom cache instances via `initAI({ cache })` or `Tempo.init({ cache })`!
+### Extensible Caching & Async Storage Adapters (`AiCacheAdapter`)
-You can provide any object that implements the standard **synchronous** `Map` interface (`get`, `set`, `has`, `delete`). Note that all cache adapter methods must execute synchronously, as the cache lookup engine does not await promise-returning cache operations.
+By default, parsed AI responses are cached in memory using `Tempo.cache` (`BoundedCache`). For distributed serverless environments (e.g. Next.js, Cloudflare Workers, Express) or cluster nodes, you can pass a custom synchronous or asynchronous storage adapter (`AiCacheAdapter`):
```typescript
-// Custom synchronous cache implementation
+import { initAI, parseAI, type AiCacheAdapter } from '@magmacomputing/tempo-plugin-ai';
+import { Redis } from '@upstash/redis';
+
+const redis = new Redis({ url: process.env.UPSTASH_URL!, token: process.env.UPSTASH_TOKEN! });
+
+// Implement custom async Redis storage adapter
+const redisAdapter: AiCacheAdapter = {
+ get: async (key) => (await redis.get(key)) ?? undefined,
+ set: async (key, value, ttlMs) => {
+ if (ttlMs) await redis.set(key, value, { px: ttlMs });
+ else await redis.set(key, value);
+ },
+ delete: async (key) => { await redis.del(key); },
+ clear: async () => { /* optional prefix wipe */ }
+};
+
initAI({
- providers: [{ id: 'groq', key: '...' }],
- cache: new MyCustomSyncCache()
+ providers: [{ id: 'groq', key: process.env.GROQ_API_KEY!, ttl: 7200000 }], // Provider-specific TTL (2 hours)
+ cacheAdapter: redisAdapter,
+ ttl: 3600000 // Global default TTL (1 hour)
});
+
+// Call-site TTL override (15 minutes)
+const dt = await parseAI("next Monday at 9am", { ttl: 900000 });
```
+
+### Cascading TTL Resolution Policies
+
+The plugin calculates cache TTL per entry using a strict resolution hierarchy:
+1. **Call-site `options.ttl`**: `parseAI(prompt, { ttl: 900000 })`
+2. **Provider-level `provider.ttl`**: `providers: [{ id: 'groq', ttl: 7200000 }]`
+3. **Global `initAI({ ttl: 3600000 })`**
+4. **Default TTL**: `3,600,000` ms (1 hour)
+
+### Fail-Open Cache Resilience
+
+Custom storage adapter calls (`adapter.get` and `adapter.set`) are wrapped in safe error handlers. If an external Redis instance crashes or encounters a network partition, the plugin logs a debug warning (if `debug: true`) and gracefully fails open to direct LLM resolution without crashing the application request.
diff --git a/packages/plugins/ai/package.json b/packages/plugins/ai/package.json
index a580adec..ff5c79a1 100644
--- a/packages/plugins/ai/package.json
+++ b/packages/plugins/ai/package.json
@@ -1,6 +1,6 @@
{
"name": "@magmacomputing/tempo-plugin-ai",
- "version": "0.2.0",
+ "version": "0.3.0",
"description": "Tempo community plugin for LLM-powered natural language parsing.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
diff --git a/packages/plugins/ai/plan/v0.3.0-roadmap.md b/packages/plugins/ai/plan/v0.3.0-roadmap.md
index fc3e0aa4..3c08d420 100644
--- a/packages/plugins/ai/plan/v0.3.0-roadmap.md
+++ b/packages/plugins/ai/plan/v0.3.0-roadmap.md
@@ -4,103 +4,27 @@ This document captures the planned feature set, architectural requirements, and
---
-## 1. Remote Provider Manifest & Dynamic Defaults
-
-### Background & Objective
-AI provider model identifiers, endpoints, and parameters update frequently. Hardcoding model defaults into the published NPM package requires frequent patch releases. v0.3.0 will introduce remote provider manifest fetching.
-
-### Requirements & Architecture
-* **Hosted Manifest Endpoint**: Host a static `providers.v1.json` manifest on the Firebase-hosted Tempo Registry (`https://registry.tempo.dev/ai/providers.v1.json`).
-* **Lifecycle & Caching**:
- * Fetch occurs **once** per application lifecycle / module load (lazy-evaluated on first `initAI()` call).
- * Manifest response is cached in module-scoped memory (`_remoteDefaults`).
- * Re-calling `initAI()` reads from in-memory cache without triggering new network requests.
-* **Fail-Open & Offline Support**:
- * If the network request fails, times out, or the client is offline/air-gapped, `initAI()` synchronously falls back to compiled local `DEFAULT_PROVIDERS`.
-* **Developer Override Options**:
- ```typescript
- initAI({
- providers: [{ id: 'openai', key: '...' }],
- remoteConfigUrl: 'https://custom-registry.internal.net/ai/providers.json', // Custom endpoint
- fetchDefaults: async (providerId) => { ... } // Custom resolver hook
- });
- ```
-
----
-
-## 2. Implementation of Scaffolded AI Function Handlers
+## 1. Implementation of Scaffolded AI Function Handlers
In v0.2.0, upcoming function handlers were scaffolded with `@internal` JSDoc tags and `not yet implemented` guards. v0.3.0 will implement the following functions:
-### 2.1 `formatAI(tempo: Tempo, prompt: string, options?: AiOptions): Promise`
+### 1.1 `formatAI(tempo: Tempo, prompt: string, options?: AiOptions): Promise`
* Formats a `Tempo` instance into human-friendly, contextual narrative text tailored to UI tones or relative countdowns.
* **Example**: `"this Friday at 5:00 PM EST (in 5 days)"`.
-### 2.2 `extractAI(text: string, options?: AiOptions): Promise`
+### 1.2 `extractAI(text: string, options?: AiOptions): Promise`
* Scans unstructured text (emails, transcripts, task notes) to extract embedded temporal entities into structured `TempoEvent` records (`label`, `start`, `end`, `type`).
-### 2.3 `diffAI(start: Tempo, end: Tempo, prompt?: string, options?: AiOptions): Promise`
+### 1.3 `diffAI(start: Tempo, end: Tempo, prompt?: string, options?: AiOptions): Promise`
* Calculates and summarizes the delta between two `Tempo` instances in human, business, or operational terms (e.g., `"6 working business days (48 hours)"`).
-### 2.4 `scheduleAI(prompt: string, options?: AiScheduleOptions): Promise`
+### 1.4 `scheduleAI(prompt: string, options?: AiScheduleOptions): Promise`
* Resolves natural language scheduling prompts against working hours, existing calendar events, and timezones into an optimal start/end `Tempo` interval.
-### 2.5 `recurrenceAI(prompt: string, options?: AiOptions): Promise`
+### 1.5 `recurrenceAI(prompt: string, options?: AiOptions): Promise`
* Translates complex natural language repeating schedule descriptions into standard RRULE strings and `Tempo` instance date generators (`rule.next(count)`).
-### 2.6 `contextAI(text: string, options?: AiOptions): Promise`
+### 1.6 `contextAI(text: string, options?: AiOptions): Promise`
* Infers `timeZone`, `locale`, and preferred `calendar` system from ambiguous location descriptions or user bios.
---
-
-## 3. Telemetry & Token Usage Tracking
-
-Extend the `.ai` metadata property attached to returned `Tempo` instances to include token consumption metrics:
-```typescript
-interface AiMeta {
- provider: string;
- cached: boolean;
- confidence: number;
- ambiguous: boolean;
- granularity: string;
- usage?: {
- promptTokens: number;
- completionTokens: number;
- totalTokens: number;
- };
-}
-```
-
----
-
-## 4. Advanced Cache TTL & Eviction Policies
-
-* Allow per-provider or per-request TTL configurations in `initAI({ ttl: 3600000 })`.
-* Support optional async storage adapters (e.g., Redis / KV stores) via explicit async wrapper interfaces.
-
----
-
-## 5. Tempo Config Assistant (`configAI`) — Strategic & Technical Analysis
-
-### 5.1 Objective & Overview
-`configAI(prompt: string, options?: AiOptions): Promise` is a proposed developer-facing function to translate natural language configuration requirements (e.g., *"Set up fiscal quarters starting Nov 1 and a layout for short weekday with 12-hour time"*) into valid Tempo runtime configurations (`Aliases`, `Layouts`, `Snippets`, `Formats`, or `TermPlugin` declarations).
-
-### 5.2 Business & Consulting Revenue Impact
-* **Consulting & Support Cannibalization**: Providing automated, self-serve AI config generation reduces developer reliance on Magma's high-margin professional services, enterprise consulting, and tier-3 support packages for custom domain-math integrations.
-* **Monetization & Upsell Safeguards**: If implemented, `configAI` must act as an upsell vehicle, surfacing recommendations for Magma's licensed premium plugins (e.g., `@magmacomputing/tempo-plugin-ticker`, `@magmacomputing/tempo-plugin-sync`) whenever multi-interval scheduling or atomic clock synchronization is requested.
-
-### 5.3 Technical Context Gap & Smart Endpoint Requirement
-* **The LLM Knowledge Gap**: Because `Tempo` introduces novel Temporal concepts, custom tokens (`{yw}`, `{wy}`, `{eon}`), and zero-cost proxy getters not broadly indexed across web codebases, standard foundation models (e.g., generic ChatGPT/Chrome LLM queries) lack sufficient context to generate valid Tempo code without hallucinating syntaxes.
-* **Dedicated RAG / System-Prompt Endpoint**: To function accurately, `configAI` requires either:
- * An embedded, highly compressed system prompt containing Tempo's exact token/layout grammar.
- * A managed backend RAG endpoint (`https://registry.tempo.dev/ai/config`) hosted via Magma's license registry infrastructure.
-
-### 5.4 Ecosystem Adoption Lifecycle
-* **Early Adoption (High Value)**: During early ecosystem growth, `configAI` serves as a critical bridge while public LLM training sets lack native Tempo awareness.
-* **Widespread Acceptance (Declining Relevance)**: As Tempo adoption scales across public repositories and developer docs, foundation models will naturally absorb Tempo's API surface into their training sets. Over time, generic LLMs will generate valid Tempo configurations out-of-the-box, rendering a specialized `configAI` assistant progressively redundant.
-
-### 5.5 Verdict & Recommended Strategy: Deprioritized (Prefer `llms.txt`)
-* **Decision**: **Do not build a runtime `configAI()` endpoint.** The ongoing engineering cost, RAG hosting infrastructure, and revenue cannibalization risks far outweigh the short-term benefits.
-* **Low-Cost Alternative (`llms.txt` / Cursor Rules)**: Rather than maintaining a custom AI endpoint, publish an official `llms.txt` and Cursor/Copilot context file alongside the VitePress documentation. Developer IDEs (Cursor, Copilot, ChatGPT) will consume the docs directly at zero infrastructure cost to Magma, leaving engineering resources focused on core performance and licensed premium plugins.
-
-
diff --git a/packages/plugins/ai/src/core/init.ts b/packages/plugins/ai/src/core/init.ts
index 4af961d0..6294821b 100644
--- a/packages/plugins/ai/src/core/init.ts
+++ b/packages/plugins/ai/src/core/init.ts
@@ -1,5 +1,6 @@
import { Tempo } from '@magmacomputing/tempo';
-import { DEFAULT_PROVIDERS } from './config.js';
+
+import { getResolvedProviderDefaults, loadRemoteManifest } from './manifest.js';
import { normalizeCacheInput, assertNoReservedProviderId } from './support.js';
import type { AiConfig, AiRateLimits, AiProvider } from './types.js';
@@ -15,9 +16,14 @@ export function initAI(config: AiConfig): void {
if (config.providers)
assertNoReservedProviderId(config.providers);
+ const remoteUrl = config.remoteConfigUrl ?? _state.config.remoteConfigUrl;
+
+ if (remoteUrl !== false)
+ loadRemoteManifest(remoteUrl, undefined, config.debug ?? _state.config.debug).catch(() => { });
+
const resolvedProviders = config.providers ? config.providers.map(p => {
const normalizedId = p.id?.toLowerCase() ?? '';
- const defaults = DEFAULT_PROVIDERS[normalizedId] || DEFAULT_PROVIDERS.openai;
+ const defaults = getResolvedProviderDefaults(normalizedId, remoteUrl, config.debug ?? _state.config.debug);
return {
...defaults,
...p
@@ -31,11 +37,23 @@ export function initAI(config: AiConfig): void {
};
if (config.cache) {
- Tempo.init({ cache: config.cache as any });
+ Tempo.init({ cache: config.cache, silent: true });
}
}
-export function clearAiCache(input: string | string[]): void {
+export function clearAiCache(input?: string | string[]): void {
+ const adapter = _state.config.cacheAdapter;
+
+ if (!input) {
+ if (adapter?.clear) {
+ try {
+ const res = adapter.clear();
+ if (res instanceof Promise) res.catch(() => {});
+ } catch {}
+ }
+ return;
+ }
+
const inputs = Array.isArray(input) ? input : [input];
for (const i of inputs) {
const normalized = normalizeCacheInput(i);
@@ -43,6 +61,21 @@ export function clearAiCache(input: string | string[]): void {
Tempo.cache.delete(normalized);
Tempo.cache.delete(i);
Tempo.cache.deletePrefix(prefix);
+
+ if (adapter) {
+ try {
+ if (adapter.delete) {
+ const res1 = adapter.delete(normalized);
+ if (res1 instanceof Promise) res1.catch(() => {});
+ const res2 = adapter.delete(i);
+ if (res2 instanceof Promise) res2.catch(() => {});
+ }
+ if (adapter.clear) {
+ const resClear = adapter.clear(prefix);
+ if (resClear instanceof Promise) resClear.catch(() => {});
+ }
+ } catch {}
+ }
}
}
diff --git a/packages/plugins/ai/src/core/manifest.ts b/packages/plugins/ai/src/core/manifest.ts
new file mode 100644
index 00000000..d9a15a58
--- /dev/null
+++ b/packages/plugins/ai/src/core/manifest.ts
@@ -0,0 +1,109 @@
+import { DEFAULT_PROVIDERS } from './config.js';
+import type { AiProvider } from './types.js';
+
+export const DEFAULT_REMOTE_MANIFEST_URL = 'https://tempo.magmacomputing.com.au/providers.v1.json';
+export const DEFAULT_MANIFEST_TIMEOUT_MS = 1500;
+
+let _cachedManifest: Record> | null = null;
+let _fetchPromise: Promise> | null> | null = null;
+
+/**
+ * Resets the in-memory manifest cache (used primarily for unit testing).
+ */
+export function resetManifestCache(): void {
+ _cachedManifest = null;
+ _fetchPromise = null;
+}
+
+/**
+ * Fetches the remote AI provider manifest once per module load.
+ * Fail-open: if network fails or times out, returns null and allows fallback to local DEFAULT_PROVIDERS.
+ */
+export async function loadRemoteManifest(
+ remoteConfigUrl: string | false = DEFAULT_REMOTE_MANIFEST_URL,
+ timeoutMs: number = DEFAULT_MANIFEST_TIMEOUT_MS,
+ debug: boolean = false
+): Promise> | null> {
+ if (remoteConfigUrl === false) {
+ return null;
+ }
+
+ if (_cachedManifest !== null) {
+ return _cachedManifest;
+ }
+
+ if (_fetchPromise !== null) {
+ return _fetchPromise;
+ }
+
+ const targetUrl = typeof remoteConfigUrl === 'string' && remoteConfigUrl.trim().length > 0
+ ? remoteConfigUrl
+ : DEFAULT_REMOTE_MANIFEST_URL;
+
+ _fetchPromise = (async () => {
+ try {
+ const controller = new AbortController();
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
+
+ const response = await fetch(targetUrl, {
+ signal: controller.signal,
+ headers: { Accept: 'application/json' }
+ });
+
+ clearTimeout(timer);
+
+ if (!response.ok) {
+ if (debug) {
+ console.warn(`[tempo-plugin-ai] Remote manifest fetch failed with status ${response.status}`);
+ }
+ _cachedManifest = {};
+ return null;
+ }
+
+ const data = await response.json();
+ if (data && typeof data === 'object' && data.providers && typeof data.providers === 'object') {
+ _cachedManifest = data.providers as Record>;
+ return _cachedManifest;
+ }
+
+ if (debug) {
+ console.warn('[tempo-plugin-ai] Remote manifest missing valid "providers" object structure');
+ }
+ _cachedManifest = {};
+ return null;
+ } catch (err: any) {
+ if (debug) {
+ console.warn(`[tempo-plugin-ai] Remote manifest fetch error: ${err?.message || err}`);
+ }
+ // Fail-open: store empty object so we fallback to DEFAULT_PROVIDERS without hanging subsequent calls
+ _cachedManifest = {};
+ return null;
+ } finally {
+ _fetchPromise = null;
+ }
+ })();
+
+ return _fetchPromise;
+}
+
+/**
+ * Resolves the default settings for a given provider ID by combining compiled DEFAULT_PROVIDERS
+ * with remote manifest entries if available.
+ */
+export function getResolvedProviderDefaults(
+ providerId: string,
+ remoteConfigUrl?: string | false,
+ debug: boolean = false
+): Partial {
+ const normalizedId = providerId?.toLowerCase() ?? '';
+ const localDefaults = DEFAULT_PROVIDERS[normalizedId] || DEFAULT_PROVIDERS.openai;
+
+ if (remoteConfigUrl === false || !_cachedManifest || !_cachedManifest[normalizedId]) {
+ return localDefaults;
+ }
+
+ return {
+ ...localDefaults,
+ ..._cachedManifest[normalizedId]
+ };
+}
diff --git a/packages/plugins/ai/src/core/support.ts b/packages/plugins/ai/src/core/support.ts
index 6a89c8c0..029273d5 100644
--- a/packages/plugins/ai/src/core/support.ts
+++ b/packages/plugins/ai/src/core/support.ts
@@ -61,7 +61,8 @@ export async function fetchFromProvider(
str: string,
contextString: string,
isDebug: boolean,
- parentSignal?: AbortSignal
+ parentSignal?: AbortSignal,
+ timeoutOverride?: number
): Promise<{ rawContent: string; providerId: string; rateLimits: ReturnType }> {
const url = provider.url!;
const model = provider.model!;
@@ -94,7 +95,7 @@ Do not include markdown blocks or any text outside the JSON.`;
const tokenLimit = { [tokenParam]: 250 };
const controller = new AbortController();
- const timeoutMs = provider.options?.timeout ?? 15000;
+ const timeoutMs = timeoutOverride ?? provider.options?.timeout ?? _state.config.timeout ?? 15000;
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
const onParentAbort = () => controller.abort();
diff --git a/packages/plugins/ai/src/core/types.ts b/packages/plugins/ai/src/core/types.ts
index 83e5e1f6..273c685f 100644
--- a/packages/plugins/ai/src/core/types.ts
+++ b/packages/plugins/ai/src/core/types.ts
@@ -24,6 +24,8 @@ export interface TempoAiMeta {
readonly rawPrompt?: string | undefined;
/** Normalized prompt input (only included when debug: true) */
readonly normalizedPrompt?: string | undefined;
+ /** Rate limit snapshot returned by the provider HTTP headers for this request */
+ readonly limits?: AiRateLimits | undefined;
}
declare module '@magmacomputing/tempo' {
@@ -33,6 +35,21 @@ declare module '@magmacomputing/tempo' {
}
}
+/**
+ * ## AiCacheAdapter
+ * Interface for synchronous or asynchronous custom storage engines (e.g. Redis, Cloudflare KV, Memcached).
+ */
+export interface AiCacheAdapter {
+ /** Retrieve a value by key */
+ get(key: string): Promise | string | undefined;
+ /** Store a value by key with optional TTL in milliseconds */
+ set(key: string, value: string, ttlMs?: number): Promise | void;
+ /** Delete a specific entry by key */
+ delete?(key: string): Promise | void;
+ /** Clear entries, optionally matching a key prefix */
+ clear?(prefix?: string): Promise | void;
+}
+
/**
* ## AiProvider
* Represents an LLM provider and its respective BYOK API key.
@@ -48,6 +65,8 @@ export interface AiProvider {
model?: string;
/** Optional parameter name for max token limit (e.g. 'max_tokens' or 'max_completion_tokens') */
tokenParam?: string | undefined;
+ /** Optional cache TTL override in milliseconds for entries produced by this provider */
+ ttl?: number | undefined;
/** Optional LLM parameters (e.g. temperature, max_tokens, top_p) */
options?: Record;
}
@@ -71,6 +90,10 @@ export interface AiParseOptions {
force?: boolean;
/** If false, disables reading and writing to cache */
cache?: boolean;
+ /** Optional custom cache adapter engine (e.g. Redis, KV store) for this request */
+ cacheAdapter?: AiCacheAdapter;
+ /** Optional TTL override in milliseconds for cached result */
+ ttl?: number;
/** If true, logs prompt context and LLM payloads to console */
debug?: boolean;
/** Execution mode across provider farm (`AiMode.Fallback` | `AiMode.Race` | `AiMode.Consensus` or string literal) */
@@ -81,6 +104,8 @@ export interface AiParseOptions {
minConfidence?: number;
/** If true, places TempoAiError into array index position instead of rejecting batch */
softErrors?: boolean;
+ /** Optional request timeout in milliseconds (overrides provider and global timeout) */
+ timeout?: number;
/** Allow extra options */
[key: string]: any;
}
@@ -94,6 +119,16 @@ export interface AiConfig {
providers?: AiProvider[] | undefined;
/** Optional custom cache implementation for storing parsed strings */
cache?: Map | undefined;
+ /** Optional custom cache storage engine (e.g., Redis, KV store) for storing parsed strings */
+ cacheAdapter?: AiCacheAdapter | undefined;
+ /** Optional global cache TTL in milliseconds for AI parsing entries (default: 3600000ms / 1 hour) */
+ ttl?: number | undefined;
+ /** Optional global timeout in milliseconds for AI requests (default: 15000ms) */
+ timeout?: number | undefined;
+ /** Optional remote manifest URL or false to disable remote defaults (default: 'https://tempo.magmacomputing.com.au/providers.v1.json') */
+ remoteConfigUrl?: string | false | undefined;
+ /** Optional custom resolver hook to fetch provider default options by ID */
+ fetchDefaults?: ((providerId: string) => Promise | null>) | undefined;
/** If true, logs the spoon-fed LLM context prompt and raw LLM response to the console */
debug?: boolean | undefined;
}
diff --git a/packages/plugins/ai/src/functions/parse.ts b/packages/plugins/ai/src/functions/parse.ts
index 309c0b99..c32bb119 100644
--- a/packages/plugins/ai/src/functions/parse.ts
+++ b/packages/plugins/ai/src/functions/parse.ts
@@ -9,7 +9,7 @@ async function parseSingleInput(str: string, options?: AiParseOptions): Promise<
const isDebug = options?.debug ?? _state.config.debug ?? false;
const normalizedStr = normalizeCacheInput(str);
- const { force, debug, mode: aiMode, providers, minConfidence, softErrors, cache: aiCacheOption, ...coreOptions } = options || {};
+ const { force, debug, mode: aiMode, providers, minConfidence, softErrors, cache: aiCacheOption, timeout: callTimeout, ...coreOptions } = options || {};
let tz: string, cal: string, loc: string, sph: string, anchorStr: string;
if (Tempo.isTempo(options?.anchor)) {
@@ -30,15 +30,37 @@ async function parseSingleInput(str: string, options?: AiParseOptions): Promise<
const anchorTempo = new Tempo(anchorStr, { ...coreOptions, timeZone: tz, calendar: cal, locale: loc, sphere: sph as any });
const cacheSalt = anchorTempo.format('{yyyy}-{mm}-{dd}');
const cacheKey = `${normalizedStr}::${cacheSalt}::${tz}::${cal}::${loc}::${sph}`;
+ const adapter = options?.cacheAdapter ?? _state.config.cacheAdapter;
let cachedIso: string | undefined;
if (!force && aiCacheOption !== false) {
- if (Tempo.cache.has(cacheKey)) {
- cachedIso = Tempo.cache.get(cacheKey);
- } else if (Tempo.cache.has(normalizedStr)) {
- cachedIso = Tempo.cache.get(normalizedStr);
- } else if (Tempo.cache.has(str)) {
- cachedIso = Tempo.cache.get(str);
+ if (adapter) {
+ try {
+ const val1 = await adapter.get(cacheKey);
+ if (val1) {
+ cachedIso = val1;
+ } else {
+ const val2 = await adapter.get(normalizedStr);
+ if (val2) {
+ cachedIso = val2;
+ } else {
+ const val3 = await adapter.get(str);
+ if (val3) cachedIso = val3;
+ }
+ }
+ } catch (err: any) {
+ if (isDebug) console.log('[tempo-plugin-ai] Cache adapter read error:', err?.message);
+ }
+ }
+
+ if (!cachedIso) {
+ if (Tempo.cache.has(cacheKey)) {
+ cachedIso = Tempo.cache.get(cacheKey);
+ } else if (Tempo.cache.has(normalizedStr)) {
+ cachedIso = Tempo.cache.get(normalizedStr);
+ } else if (Tempo.cache.has(str)) {
+ cachedIso = Tempo.cache.get(str);
+ }
}
}
@@ -99,7 +121,7 @@ async function parseSingleInput(str: string, options?: AiParseOptions): Promise<
for (const provider of availableProviders) {
try {
- const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug);
+ const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug, undefined, callTimeout);
const cleanContent = rawContent.replace(/^```json\s*/i, '').replace(/\s*```$/i, '');
const parsedData = JSON.parse(cleanContent);
@@ -135,7 +157,7 @@ async function parseSingleInput(str: string, options?: AiParseOptions): Promise<
const parentController = new AbortController();
try {
const promises = availableProviders.map(async (provider) => {
- const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug, parentController.signal);
+ const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug, parentController.signal, callTimeout);
const cleanContent = rawContent.replace(/^```json\s*/i, '').replace(/\s*```$/i, '');
return { parsedData: JSON.parse(cleanContent), providerId, rateLimits };
});
@@ -148,7 +170,7 @@ async function parseSingleInput(str: string, options?: AiParseOptions): Promise<
} else if (mode === AiMode.Consensus) {
const promises = availableProviders.map(async (provider) => {
- const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug);
+ const { rawContent, providerId, rateLimits } = await fetchFromProvider(provider, str, contextString, isDebug, undefined, callTimeout);
const cleanContent = rawContent.replace(/^```json\s*/i, '').replace(/\s*```$/i, '');
return { parsedData: JSON.parse(cleanContent), providerId, rateLimits };
});
@@ -195,7 +217,7 @@ async function parseSingleInput(str: string, options?: AiParseOptions): Promise<
_state.limits = successfulResult?.rateLimits ?? null;
- const { parsedData, providerId } = successfulResult!;
+ const { parsedData, providerId, rateLimits } = successfulResult!;
const rawIso = typeof parsedData?.iso === 'string' ? parsedData.iso : 'INVALID';
const confidence = typeof parsedData?.confidence === 'number' ? parsedData.confidence : (rawIso === 'INVALID' ? 0.0 : 1.0);
const ambiguous = Boolean(parsedData?.ambiguous || rawIso === 'INVALID');
@@ -215,13 +237,26 @@ async function parseSingleInput(str: string, options?: AiParseOptions): Promise<
rawIso: rawIso === 'INVALID' ? 'INVALID' : rawIso,
reasoning: isDebug ? reasoning : undefined,
rawPrompt: isDebug ? str : undefined,
- normalizedPrompt: isDebug ? normalizedStr : undefined
+ normalizedPrompt: isDebug ? normalizedStr : undefined,
+ limits: rateLimits ?? undefined
});
}
const parsedIso = `${rawIso.replace(/Z$/i, '')}[${tz}]`;
+ // Determine TTL hierarchy: options.ttl > provider.ttl > global config.ttl > 3600000 (1 hour)
+ const winningProvider = availableProviders.find(p => p.id === providerId);
+ const resolvedTtl = options?.ttl ?? winningProvider?.ttl ?? _state.config.ttl ?? 3600000;
+
if (aiCacheOption !== false) {
+ if (adapter) {
+ try {
+ const res = adapter.set(cacheKey, parsedIso, resolvedTtl);
+ if (res instanceof Promise) await res;
+ } catch (err: any) {
+ if (isDebug) console.log('[tempo-plugin-ai] Cache adapter write error:', err?.message);
+ }
+ }
Tempo.cache.set(cacheKey, parsedIso);
}
@@ -235,7 +270,8 @@ async function parseSingleInput(str: string, options?: AiParseOptions): Promise<
rawIso,
reasoning: isDebug ? reasoning : undefined,
rawPrompt: isDebug ? str : undefined,
- normalizedPrompt: isDebug ? normalizedStr : undefined
+ normalizedPrompt: isDebug ? normalizedStr : undefined,
+ limits: rateLimits ?? undefined
});
}
diff --git a/packages/plugins/ai/src/index.ts b/packages/plugins/ai/src/index.ts
index fccea421..1fc5b274 100644
--- a/packages/plugins/ai/src/index.ts
+++ b/packages/plugins/ai/src/index.ts
@@ -2,6 +2,7 @@
export { TempoAiError } from './core/error.js';
export * from './core/types.js';
export * from './core/config.js';
+export { loadRemoteManifest, resetManifestCache, DEFAULT_REMOTE_MANIFEST_URL } from './core/manifest.js';
export { initAI, clearAiCache, getAiRateLimits } from './core/init.js';
// AI Function Handlers
diff --git a/packages/plugins/ai/test/cache.test.ts b/packages/plugins/ai/test/cache.test.ts
new file mode 100644
index 00000000..7f081381
--- /dev/null
+++ b/packages/plugins/ai/test/cache.test.ts
@@ -0,0 +1,139 @@
+import { parseAI, initAI, clearAiCache, type AiCacheAdapter } from '../src/index.js';
+import { Tempo } from '@magmacomputing/tempo';
+
+describe('Advanced Cache TTL & Async Storage Adapters', () => {
+ beforeEach(() => {
+ vi.restoreAllMocks();
+ Tempo.cache.clear();
+ initAI({
+ providers: [{ id: 'groq', key: 'mock-test-key' }],
+ remoteConfigUrl: false
+ });
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ Tempo.cache.clear();
+ });
+
+ it('should read from and write to custom async AiCacheAdapter', async () => {
+ const store = new Map();
+ const ttlLogs: number[] = [];
+
+ const mockAdapter: AiCacheAdapter = {
+ get: vi.fn(async (key: string) => store.get(key)),
+ set: vi.fn(async (key: string, value: string, ttlMs?: number) => {
+ store.set(key, value);
+ if (ttlMs) ttlLogs.push(ttlMs);
+ }),
+ delete: vi.fn(async (key: string) => {
+ store.delete(key);
+ }),
+ clear: vi.fn(async () => {
+ store.clear();
+ })
+ };
+
+ initAI({
+ providers: [{ id: 'groq', key: 'mock-test-key' }],
+ cacheAdapter: mockAdapter,
+ ttl: 120000
+ });
+
+ // Mock LLM fetch response for first call
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
+ new Response(JSON.stringify({
+ choices: [{ message: { content: '{"reasoning":"Custom adapter test", "iso":"2026-11-26T00:00:00", "confidence":0.95}' } }]
+ }), { status: 200 })
+ );
+
+ const result1 = await parseAI('Thanksgiving 2026');
+ expect(result1.isValid).toBe(true);
+ expect(result1.ai?.provider).toBe('groq');
+ expect(mockAdapter.set).toHaveBeenCalled();
+ expect(ttlLogs).toContain(120000);
+
+ // Second call should hit the custom adapter
+ const result2 = await parseAI('Thanksgiving 2026');
+ expect(result2.isValid).toBe(true);
+ expect(result2.ai?.provider).toBe('cache');
+ expect(result2.ai?.cached).toBe(true);
+ expect(mockAdapter.get).toHaveBeenCalled();
+ });
+
+ it('should observe TTL resolution hierarchy (options.ttl > provider.ttl > global config.ttl)', async () => {
+ const setTtlLogs: number[] = [];
+
+ const mockAdapter: AiCacheAdapter = {
+ get: vi.fn(() => undefined),
+ set: vi.fn((_key: string, _val: string, ttlMs?: number) => {
+ if (ttlMs) setTtlLogs.push(ttlMs);
+ })
+ };
+
+ initAI({
+ providers: [{ id: 'groq', key: 'mock-key', ttl: 60000 }],
+ cacheAdapter: mockAdapter,
+ ttl: 300000
+ });
+
+ vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(
+ new Response(JSON.stringify({
+ choices: [{ message: { content: '{"reasoning":"TTL test", "iso":"2026-12-25T00:00:00", "confidence":0.95}' } }]
+ }), { status: 200 })
+ ));
+
+ // Call 1: Inherits provider.ttl (60000)
+ await parseAI('Christmas 2026');
+ expect(setTtlLogs[0]).toBe(60000);
+
+ // Call 2: Call-site options.ttl (15000) overrides provider and global TTL
+ await parseAI('Christmas 2026', { force: true, ttl: 15000 });
+ expect(setTtlLogs[1]).toBe(15000);
+ });
+
+ it('should fail-open and fetch from LLM if custom cacheAdapter throws a network error', async () => {
+ const faultyAdapter: AiCacheAdapter = {
+ get: vi.fn(async () => {
+ throw new Error('Redis connection refused');
+ }),
+ set: vi.fn(async () => {
+ throw new Error('Redis write error');
+ })
+ };
+
+ initAI({
+ providers: [{ id: 'groq', key: 'mock-key' }],
+ cacheAdapter: faultyAdapter
+ });
+
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
+ new Response(JSON.stringify({
+ choices: [{ message: { content: '{"reasoning":"Faulty adapter test", "iso":"2026-07-04T00:00:00", "confidence":0.95}' } }]
+ }), { status: 200 })
+ );
+
+ // parseAI should NOT throw Redis error; it should fail open to LLM fetch
+ const result = await parseAI('Independence Day 2026');
+ expect(result.isValid).toBe(true);
+ expect(result.ai?.provider).toBe('groq');
+ });
+
+ it('should clear custom cacheAdapter entries when clearAiCache is invoked', async () => {
+ const mockAdapter: AiCacheAdapter = {
+ get: vi.fn(),
+ set: vi.fn(),
+ delete: vi.fn(),
+ clear: vi.fn()
+ };
+
+ initAI({ cacheAdapter: mockAdapter });
+
+ clearAiCache('Easter 2026');
+ expect(mockAdapter.delete).toHaveBeenCalled();
+ expect(mockAdapter.clear).toHaveBeenCalled();
+
+ clearAiCache();
+ expect(mockAdapter.clear).toHaveBeenCalledTimes(2);
+ });
+});
diff --git a/packages/plugins/ai/test/index.spec.ts b/packages/plugins/ai/test/index.spec.ts
index 21599b73..2a60f363 100644
--- a/packages/plugins/ai/test/index.spec.ts
+++ b/packages/plugins/ai/test/index.spec.ts
@@ -8,6 +8,10 @@ describe('AI Parsing Plugin', () => {
const isLiveTest = Boolean(process.env.LIVE_AI_TEST && liveApiKey);
beforeEach(() => {
+ vi.spyOn(console, 'warn').mockImplementation(() => {});
+ vi.spyOn(console, 'error').mockImplementation(() => {});
+ vi.spyOn(console, 'log').mockImplementation(() => {});
+
if (isLiveTest) {
initAI({
providers: [{ id: liveProviderId, key: liveApiKey! }]
@@ -20,7 +24,7 @@ describe('AI Parsing Plugin', () => {
});
afterEach(() => {
- vi.restoreAllMocks();
+ vi.clearAllMocks();
});
it('should fall back to native parsing first and attach .ai metadata', async () => {
@@ -504,6 +508,26 @@ describe('AI Parsing Plugin', () => {
expect(getAiRateLimits()?.resetAt?.isValid).toBe(true);
});
+ it('should attach limits snapshot directly to the returned Tempo instance .ai property', async () => {
+ initAI({ providers: [{ id: 'groq', key: 'test-key' }] });
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
+
+ fetchSpy.mockResolvedValueOnce(new Response(JSON.stringify({
+ choices: [{ message: { content: '{"iso":"2026-11-26T00:00:00"}' } }]
+ }), {
+ status: 200,
+ headers: new Headers({
+ 'x-ratelimit-remaining-requests': '499',
+ 'x-ratelimit-remaining-tokens': '99500'
+ })
+ }));
+
+ const result = await parseAI('Thanksgiving 2026', { force: true });
+ expect(result.ai?.limits).toBeDefined();
+ expect(result.ai?.limits?.remainingRequests).toBe(499);
+ expect(result.ai?.limits?.remainingTokens).toBe(99500);
+ });
+
it('should ignore invalid or malformed duration strings without throwing or crashing', async () => {
initAI({ providers: [{ id: 'openai', key: 'test-key' }] });
const fetchSpy = vi.spyOn(globalThis, 'fetch');
diff --git a/packages/plugins/ai/test/manifest.test.ts b/packages/plugins/ai/test/manifest.test.ts
new file mode 100644
index 00000000..ff827ae6
--- /dev/null
+++ b/packages/plugins/ai/test/manifest.test.ts
@@ -0,0 +1,142 @@
+import {
+ initAI,
+ loadRemoteManifest,
+ resetManifestCache,
+ DEFAULT_REMOTE_MANIFEST_URL,
+ DEFAULT_PROVIDERS
+} from '../src/index.js';
+
+describe('Remote Provider Manifest & Dynamic Defaults', () => {
+ beforeEach(() => {
+ resetManifestCache();
+ vi.restoreAllMocks();
+ });
+
+ afterEach(() => {
+ resetManifestCache();
+ vi.restoreAllMocks();
+ });
+
+ it('should fetch remote manifest and cache in memory for subsequent calls', async () => {
+ const mockManifest = {
+ version: '1.0',
+ providers: {
+ groq: { model: 'llama-3.3-70b-versatile', tokenParam: 'max_tokens' },
+ openai: { model: 'gpt-5.4-mini', tokenParam: 'max_completion_tokens' }
+ }
+ };
+
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
+ new Response(JSON.stringify(mockManifest), { status: 200 })
+ );
+
+ const result1 = await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL);
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ expect(fetchSpy).toHaveBeenCalledWith(
+ DEFAULT_REMOTE_MANIFEST_URL,
+ expect.objectContaining({ headers: { Accept: 'application/json' } })
+ );
+ expect(result1).toEqual(mockManifest.providers);
+
+ // Second call should return cached object without second fetch
+ const result2 = await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL);
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ expect(result2).toEqual(mockManifest.providers);
+ });
+
+ it('should gracefully fail-open on network error (500) and return null', async () => {
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
+ new Response(null, { status: 500, statusText: 'Internal Server Error' })
+ );
+
+ const result = await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL);
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ expect(result).toBeNull();
+ });
+
+ it('should gracefully fail-open on network timeout / abort', async () => {
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockImplementationOnce(() => {
+ const err = new Error('The operation was aborted');
+ err.name = 'AbortError';
+ return Promise.reject(err);
+ });
+
+ const result = await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL, 100);
+ expect(fetchSpy).toHaveBeenCalledTimes(1);
+ expect(result).toBeNull();
+ });
+
+ it('should return null immediately if remoteConfigUrl is false', async () => {
+ const fetchSpy = vi.spyOn(globalThis, 'fetch');
+ const result = await loadRemoteManifest(false);
+ expect(fetchSpy).not.toHaveBeenCalled();
+ expect(result).toBeNull();
+ });
+
+ it('should fetch custom remoteConfigUrl when provided', async () => {
+ const customUrl = 'https://custom-domain.net/providers.json';
+ const mockManifest = {
+ version: '1.0',
+ providers: {
+ groq: { model: 'custom-groq-model' }
+ }
+ };
+
+ const fetchSpy = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
+ new Response(JSON.stringify(mockManifest), { status: 200 })
+ );
+
+ const result = await loadRemoteManifest(customUrl);
+ expect(fetchSpy).toHaveBeenCalledWith(customUrl, expect.anything());
+ expect(result).toEqual(mockManifest.providers);
+ });
+
+ it('should resolve provider defaults from remote manifest when available in initAI', async () => {
+ const mockManifest = {
+ version: '1.0',
+ providers: {
+ groq: {
+ url: 'https://api.groq.com/openai/v1/chat/completions',
+ model: 'remote-llama-model',
+ tokenParam: 'max_tokens'
+ }
+ }
+ };
+
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
+ new Response(JSON.stringify(mockManifest), { status: 200 })
+ );
+
+ // Pre-load manifest
+ await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL);
+
+ initAI({
+ providers: [{ id: 'groq', key: 'test-key' }]
+ });
+
+ // Check resolved providers in init state
+ const { _state } = await import('../src/core/init.js');
+ expect(_state.config.providers).toHaveLength(1);
+ expect(_state.config.providers?.[0].model).toBe('remote-llama-model');
+ });
+
+ it('should fallback to compiled DEFAULT_PROVIDERS if remote manifest is missing provider ID', async () => {
+ const mockManifest = {
+ version: '1.0',
+ providers: {}
+ };
+
+ vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
+ new Response(JSON.stringify(mockManifest), { status: 200 })
+ );
+
+ await loadRemoteManifest(DEFAULT_REMOTE_MANIFEST_URL);
+
+ initAI({
+ providers: [{ id: 'openai', key: 'test-key' }]
+ });
+
+ const { _state } = await import('../src/core/init.js');
+ expect(_state.config.providers?.[0].model).toBe(DEFAULT_PROVIDERS.openai.model);
+ });
+});
diff --git a/packages/tempo/.vitepress/config.ts b/packages/tempo/.vitepress/config.ts
index 24b4bc1f..7d0fcc19 100644
--- a/packages/tempo/.vitepress/config.ts
+++ b/packages/tempo/.vitepress/config.ts
@@ -38,6 +38,7 @@ export default defineConfig({
items: [
{ text: 'Introduction', link: '/README' },
{ text: 'Installation', link: '/doc/1-getting-started/installation' },
+ { text: 'AI & IDE Integration', link: '/doc/1-getting-started/ai-integration' },
{ text: 'Cookbook', link: '/doc/1-getting-started/tempo.cookbook' }
]
},
diff --git a/packages/tempo/.vitepress/theme/data/catalog.json b/packages/tempo/.vitepress/theme/data/catalog.json
index 8814dbdb..f40e7958 100644
--- a/packages/tempo/.vitepress/theme/data/catalog.json
+++ b/packages/tempo/.vitepress/theme/data/catalog.json
@@ -51,7 +51,7 @@
"packageName": "@magmacomputing/tempo-plugin-ai",
"plan": "community",
"status": "experimental",
- "version": "0.2.0"
+ "version": "0.3.0"
},
{
"id": "ticker",
diff --git a/packages/tempo/CHANGELOG.md b/packages/tempo/CHANGELOG.md
index ba4699cb..ec1badb2 100644
--- a/packages/tempo/CHANGELOG.md
+++ b/packages/tempo/CHANGELOG.md
@@ -10,6 +10,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- **Cache Serialization (`toJSON`)**: Added native `toJSON()` serialization support to `BoundedCache` and the `Tempo.cache` facade object. Calling `Tempo.cache.toJSON()` or `JSON.stringify(Tempo.cache)` now cleanly converts active, non-expired in-memory cache entries into a plain key-value JavaScript object.
+- **AI Context & IDE Integration (`llms.txt`)**: Published official standardized `llms.txt` and `llms-full.txt` context bundles at `https://tempo.magmacomputing.com.au` to enable zero-hallucination code generation for IDE tools (Cursor, VS Code / GitHub Copilot, Antigravity) and web AI interfaces (ChatGPT, Claude, Gemini).
+- **Automated Doc Harvester**: Created `bin/generate-llms-txt.mjs` monorepo build script integrated into `npm run docs:build` to harvest all 56 markdown documentation files into a unified `llms-full.txt` corpus.
+- **AI Documentation Guide**: Added a dedicated `AI & IDE Integration` guide (`doc/1-getting-started/ai-integration.md`) featured directly in the primary VitePress navigation sidebar under Getting Started.
## [3.11.0] - 2026-07-31
diff --git a/packages/tempo/bin/expand-typedoc.mjs b/packages/tempo/bin/expand-typedoc.mjs
index 2e5d92a6..e46d93a5 100644
--- a/packages/tempo/bin/expand-typedoc.mjs
+++ b/packages/tempo/bin/expand-typedoc.mjs
@@ -6,33 +6,49 @@ import ts from 'typescript';
const __filename = fileURLToPath(import.meta.url);
const tempoDir = path.dirname(path.dirname(__filename));
const libraryDir = path.resolve(tempoDir, '../library');
-const htmlOutputDir = path.resolve(tempoDir, 'public/api/library/types');
-console.log('🔍 Running Phase 3: TypeDoc Compiler API Type Expansion Post-Processor...');
-
-// 1. Load TypeScript program for @magmacomputing/library
-const entryPoints = [
- path.resolve(libraryDir, 'src/common.index.ts'),
- path.resolve(libraryDir, 'src/browser.index.ts'),
- path.resolve(libraryDir, 'src/server.index.ts')
+console.log('🔍 Running TypeDoc Compiler API Type Expansion Post-Processor...');
+
+const targets = [
+ {
+ name: '@magmacomputing/library',
+ dir: libraryDir,
+ entryPoints: [
+ path.resolve(libraryDir, 'src/common.index.ts'),
+ path.resolve(libraryDir, 'src/browser.index.ts'),
+ path.resolve(libraryDir, 'src/server.index.ts')
+ ],
+ tsconfigPath: path.resolve(libraryDir, 'tsconfig.json'),
+ htmlOutputDir: path.resolve(tempoDir, 'public/api/library/types')
+ },
+ {
+ name: '@magmacomputing/tempo',
+ dir: tempoDir,
+ entryPoints: [
+ path.resolve(tempoDir, 'src/tempo.index.ts')
+ ],
+ tsconfigPath: path.resolve(tempoDir, 'tsconfig.build.json'),
+ htmlOutputDir: path.resolve(tempoDir, 'public/api/types')
+ }
];
-const tsconfigPath = path.resolve(libraryDir, 'tsconfig.json');
-const configFile = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
-const parsedCmd = ts.parseJsonConfigFileContent(configFile.config, ts.sys, libraryDir);
-
-const program = ts.createProgram(entryPoints, parsedCmd.options);
-const checker = program.getTypeChecker();
+for (const target of targets) {
+ if (!fs.existsSync(target.htmlOutputDir)) {
+ console.warn(`⚠️ Skipping ${target.name}: HTML output directory does not exist yet (${target.htmlOutputDir})`);
+ continue;
+ }
-// 2. Map of typeName -> expanded type declaration string
-const typeMap = new Map();
+ const configFile = ts.readConfigFile(target.tsconfigPath, ts.sys.readFile);
+ const parsedCmd = ts.parseJsonConfigFileContent(configFile.config, ts.sys, target.dir);
-for (const sourceFile of program.getSourceFiles()) {
- if (sourceFile.isDeclarationFile) continue;
+ const program = ts.createProgram(target.entryPoints, parsedCmd.options);
+ const checker = program.getTypeChecker();
+ const typeMap = new Map();
- ts.forEachChild(sourceFile, (node) => {
+ function visit(node, currentNamespace = '') {
if (ts.isTypeAliasDeclaration(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
const typeName = node.name.text;
+ const fullKey = currentNamespace ? `${currentNamespace}.${typeName}` : typeName;
const rhsNode = node.type;
const rhsType = checker.getTypeAtLocation(rhsNode);
@@ -44,7 +60,6 @@ for (const sourceFile of program.getSourceFiles()) {
ts.TypeFormatFlags.AllowUniqueESSymbolType
);
- // Fallback to node.type.getText() if typeToString returns the typeAlias identifier itself
if (expanded === typeName || expanded.startsWith(`${typeName}<`)) {
expanded = rhsNode.getText();
}
@@ -53,34 +68,40 @@ for (const sourceFile of program.getSourceFiles()) {
const fullSignature = typeParams ? `${typeName}<${typeParams}> = ${expanded}` : `${typeName} = ${expanded}`;
typeMap.set(typeName, fullSignature);
+ typeMap.set(fullKey, fullSignature);
+ } else if (ts.isModuleDeclaration(node) && node.body) {
+ const nsName = node.name.text;
+ const nextNs = currentNamespace ? `${currentNamespace}.${nsName}` : nsName;
+ ts.forEachChild(node.body, child => visit(child, nextNs));
}
- });
-}
+ }
-console.log(`Found ${typeMap.size} exported type aliases from @magmacomputing/library source.`);
+ for (const sourceFile of program.getSourceFiles()) {
+ if (sourceFile.isDeclarationFile) continue;
+ ts.forEachChild(sourceFile, node => visit(node));
+ }
-// 3. Scan generated HTML files in public/api/library/types/
-if (!fs.existsSync(htmlOutputDir)) {
- console.error(`❌ Output directory ${htmlOutputDir} does not exist. Run TypeDoc first.`);
- process.exit(1);
-}
+ console.log(`Found ${typeMap.size} type alias mappings for ${target.name}.`);
-const htmlFiles = fs.readdirSync(htmlOutputDir).filter(f => f.endsWith('.html'));
-let processedCount = 0;
+ const htmlFiles = fs.readdirSync(target.htmlOutputDir).filter(f => f.endsWith('.html'));
+ let processedCount = 0;
-for (const file of htmlFiles) {
- const filePath = path.join(htmlOutputDir, file);
- let html = fs.readFileSync(filePath, 'utf-8');
+ for (const file of htmlFiles) {
+ const filePath = path.join(target.htmlOutputDir, file);
+ let html = fs.readFileSync(filePath, 'utf-8');
- // Extract the type alias name from filename or page title (e.g. common.index.CountOf.html -> CountOf)
- const match = file.match(/common\.index\.([A-Za-z0-9_$]+)\.html$/);
- if (!match) continue;
+ // Extract type identifier from filename e.g.:
+ // - common.index.CountOf.html -> CountOf
+ // - Tempo.DateTime.html -> Tempo.DateTime or DateTime
+ // - Tempo.WEEKDAY-1.html -> Tempo.WEEKDAY or WEEKDAY
+ const cleanName = file.replace(/\.html$/, '').replace(/-\d+$/, '');
+ const parts = cleanName.split('.');
+ const typeName = parts[parts.length - 1];
- const typeName = match[1];
- const expandedSig = typeMap.get(typeName);
+ const expandedSig = typeMap.get(cleanName) || typeMap.get(typeName);
- if (expandedSig) {
- const injectionHtml = `
+ if (expandedSig && !html.includes('expanded-type-details')) {
+ const injectionHtml = `
🔍 Expanded Type Evaluation (Compiler API)
@@ -91,18 +112,18 @@ for (const file of htmlFiles) {
`;
- // Inject directly after
...
- const signatureEndIdx = html.indexOf('', html.indexOf('class="tsd-signature"'));
- if (signatureEndIdx !== -1) {
- const insertPos = signatureEndIdx + 6;
- html = html.slice(0, insertPos) + injectionHtml + html.slice(insertPos);
- fs.writeFileSync(filePath, html, 'utf-8');
- processedCount++;
+ const signatureEndIdx = html.indexOf('', html.indexOf('class="tsd-signature"'));
+ if (signatureEndIdx !== -1) {
+ const insertPos = signatureEndIdx + 6;
+ html = html.slice(0, insertPos) + injectionHtml + html.slice(insertPos);
+ fs.writeFileSync(filePath, html, 'utf-8');
+ processedCount++;
+ }
}
}
-}
-console.log(`✅ Injected expanded type definitions into ${processedCount} HTML pages in public/api/library/types/`);
+ console.log(`✅ Injected expanded type definitions into ${processedCount} HTML pages in ${path.relative(tempoDir, target.htmlOutputDir)}`);
+}
function escapeHtml(str) {
return str
diff --git a/packages/tempo/bin/generate-llms-txt.mjs b/packages/tempo/bin/generate-llms-txt.mjs
new file mode 100644
index 00000000..930c7562
--- /dev/null
+++ b/packages/tempo/bin/generate-llms-txt.mjs
@@ -0,0 +1,42 @@
+import { readdir, readFile, writeFile } from 'node:fs/promises';
+import { join, relative } from 'node:path';
+import { fileURLToPath } from 'node:url';
+
+const __dirname = fileURLToPath(new URL('.', import.meta.url));
+const docDir = join(__dirname, '../doc');
+const outputFile = join(__dirname, '../public/llms-full.txt');
+
+async function getMarkdownFiles(dir) {
+ const entries = await readdir(dir, { withFileTypes: true });
+ let files = [];
+ for (const entry of entries) {
+ const res = join(dir, entry.name);
+ if (entry.isDirectory()) {
+ files = files.concat(await getMarkdownFiles(res));
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
+ files.push(res);
+ }
+ }
+ return files.sort();
+}
+
+async function generateLlmsFull() {
+ try {
+ const files = await getMarkdownFiles(docDir);
+ let content = `# Tempo Full Documentation Context\n\n> This file contains the complete concatenated markdown documentation set for @magmacomputing/tempo. It is intended for automated LLM context ingestion and RAG indexing.\n\n---\n\n`;
+
+ for (const file of files) {
+ const relPath = relative(docDir, file);
+ const fileContent = await readFile(file, 'utf-8');
+ content += `\n\n\n# Document: ${relPath}\n\n${fileContent}\n\n\n\n---\n`;
+ }
+
+ await writeFile(outputFile, content, 'utf-8');
+ console.log(`✅ Successfully generated llms-full.txt (${files.length} markdown documents merged)`);
+ } catch (err) {
+ console.error('❌ Error generating llms-full.txt:', err);
+ process.exit(1);
+ }
+}
+
+generateLlmsFull();
diff --git a/packages/tempo/doc/1-getting-started/ai-integration.md b/packages/tempo/doc/1-getting-started/ai-integration.md
new file mode 100644
index 00000000..358897a3
--- /dev/null
+++ b/packages/tempo/doc/1-getting-started/ai-integration.md
@@ -0,0 +1,74 @@
+# 🤖 AI & IDE Integration (`llms.txt`)
+
+To ensure modern AI coding assistants—such as **Cursor**, **VS Code (GitHub Copilot)**, **Antigravity**, **ChatGPT**, and **Claude**—generate accurate, hallucination-free Tempo code, Tempo publishes an official, standardized [`llms.txt`](https://tempo.magmacomputing.com.au/llms.txt) rulebook.
+
+By providing these rules to your AI assistant, your IDE will respect Tempo's strict immutability, zero-cost getters, native `Temporal` runtime expectations, and layout token syntax out-of-the-box.
+
+---
+
+## 🚀 Quick Setup by IDE / Tool
+
+### 1. Cursor IDE
+Add Tempo to Cursor's native documentation index:
+1. Open **Cursor Settings** (`Cmd + ,` or `Ctrl + ,`).
+2. Navigate to **Features** ➔ **Docs**.
+3. Click **+ Add new doc** and enter:
+ - **Name**: `Tempo`
+ - **URL**: `https://tempo.magmacomputing.com.au/llms.txt`
+
+> [!TIP]
+> Once added, type `@Tempo` in any Cursor chat or prompt window to inject exact API syntax rules into your conversation.
+
+---
+
+### 2. VS Code & GitHub Copilot
+In VS Code, configure GitHub Copilot Chat by adding a `.github/copilot-instructions.md` file (or `.cursorrules`) to the root of your workspace:
+
+```markdown
+# Tempo AI Rules
+- Always use `Tempo` from `@magmacomputing/tempo`.
+- Never instantiate legacy JavaScript `Date`. Tempo expects native `Temporal` or polyfill.
+- All mutating methods (`.add()`, `.subtract()`, `.with()`) return a brand-new, frozen `Tempo` instance.
+- Refer to https://tempo.magmacomputing.com.au/llms.txt for full layout token grammar.
+```
+
+When prompting Copilot Chat in VS Code:
+```text
+"Using https://tempo.magmacomputing.com.au/llms.txt, write a custom layout parser..."
+```
+
+---
+
+### 3. Antigravity AI Assistant
+In Antigravity, you can reference the live endpoint directly in your chat prompt or store it as a localized Knowledge Item (KI):
+- Reference `@https://tempo.magmacomputing.com.au/llms.txt` in your prompt for instant context ingestion.
+
+---
+
+### 4. ChatGPT & Claude Projects
+For web-based LLM interfaces, reference or copy-paste the full, un-truncated documentation context file:
+👉 **[Full RAG Documentation Bundle (`llms-full.txt`)](https://tempo.magmacomputing.com.au/llms-full.txt)**
+
+---
+
+## 🛠️ Prompting AI for Custom Layout Extensions
+
+When asking AI assistants to generate custom layout patterns for parsing unique date-time formats, instruct the model to use Tempo's configuration syntax (`Tempo.config({ layouts: { ... } })`) and named capture tokens (`{yy}`, `{mon}`, `{dd}`, `{hh}`, `{mi}`, `{ss}`, `{tzd}`).
+
+### Sample Prompt:
+> *"Using the rules from https://tempo.magmacomputing.com.au/llms.txt, register a custom Tempo layout for fiscal quarters (e.g., 'Q3 2026') using `Tempo.config({ layouts: { ... } })` and parse a date using `Tempo.parse()`."*
+
+### Generated Code (Actual Tempo Syntax):
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+
+// 1. Register custom layout pattern using snippet tokens
+Tempo.config({
+ layouts: {
+ fiscal_quarter: 'Q{nbr} {yy}'
+ }
+});
+
+// 2. Parse date string using the registered layout
+const date = Tempo.parse('Q3 2026', 'fiscal_quarter');
+```
diff --git a/packages/tempo/doc/1-getting-started/installation.md b/packages/tempo/doc/1-getting-started/installation.md
index 8bf64aac..262a871e 100644
--- a/packages/tempo/doc/1-getting-started/installation.md
+++ b/packages/tempo/doc/1-getting-started/installation.md
@@ -242,3 +242,14 @@ We recommend pinning your versions in production environments to ensure stabilit
* **JSDelivr**: `https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@3/...` (Locks to major version 3)
* **Latest**: `https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/...` (Omit the version string to always receive the latest release. Note that JSDelivr will resolve a missing version tag to the latest published release).
+
+---
+
+## 🤖 AI & IDE Integration (`llms.txt`)
+
+> [!TIP]
+> Using **Cursor**, **VS Code (GitHub Copilot)**, **Antigravity**, **ChatGPT**, or **Claude**?
+> Tempo publishes an official [`llms.txt`](https://tempo.magmacomputing.com.au/llms.txt) index to give AI assistants zero-hallucination context about Tempo's syntax, token grammar, and immutability rules.
+>
+> 👉 **[Read the dedicated AI & IDE Integration Guide](./ai-integration.md)** for step-by-step setup instructions for your IDE or tool.
+
diff --git a/packages/tempo/doc/3-extending-tempo/tempo.layout.md b/packages/tempo/doc/3-extending-tempo/tempo.layout.md
index 39896a86..57b52b3f 100644
--- a/packages/tempo/doc/3-extending-tempo/tempo.layout.md
+++ b/packages/tempo/doc/3-extending-tempo/tempo.layout.md
@@ -101,6 +101,19 @@ console.log(regex.source);
---
+## 🤖 AI & LLM Layout Prompting
+
+When prompting AI assistants (Cursor, GitHub Copilot, ChatGPT, Claude) to write custom `Tempo` regular expression snippets and layout extensions:
+
+1. **Ingest AI Rules**: Provide the assistant with our official `llms.txt` rules by referencing `@https://tempo.magmacomputing.com.au/llms.txt` in Cursor or pasting `llms.txt` context into ChatGPT.
+2. **Explicit Token Request**: Ask the LLM to use Tempo's standard snippet tokens (`{yy}`, `{mon}`, `{dd}`, `{hh}`, `{mi}`, `{ss}`, `{tzd}`) rather than raw, un-anchored regular expressions.
+3. **Example AI Prompt**:
+ ```text
+ "Using https://tempo.magmacomputing.com.au/llms.txt, register a custom layout for strings like 'Q3 2026' using Tempo.config({ layouts: { ... } }) and snippet tokens."
+ ```
+
+---
+
## Professional Services
If your project involves specialized terminology, complex financial calendars, or legacy application log formats, the **Magma Computing Solutions** team offers professional services to design and test custom `Tempo` Layouts optimized for your business needs.
diff --git a/packages/tempo/img/library-logo.svg b/packages/tempo/img/library-logo.svg
index 55fb6bdf..ec7fd7c4 100644
--- a/packages/tempo/img/library-logo.svg
+++ b/packages/tempo/img/library-logo.svg
@@ -1,21 +1,34 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/packages/tempo/package.json b/packages/tempo/package.json
index c0560653..489ddf93 100644
--- a/packages/tempo/package.json
+++ b/packages/tempo/package.json
@@ -235,8 +235,8 @@
"clean": "magma-cli rm dist && (node ../../node_modules/typescript-7/bin/tsc -b --clean || true)",
"prepublishOnly": "if [ $(git rev-parse --abbrev-ref HEAD) != main ]; then echo 'ERROR: Must be on main branch to publish.'; exit 1; fi && if [ -z \"$TEMPO_LICENSE_PATH\" ] || [ ! -f \"$TEMPO_LICENSE_PATH\" ]; then echo '🚨 ERROR: TEMPO_LICENSE_PATH is missing or invalid. Cannot publish Premium build.'; exit 1; fi && npm run build",
"docs:api": "typedoc && typedoc --options typedoc.library.json && node bin/expand-typedoc.mjs",
- "docs:dev": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && vitepress dev",
- "docs:build": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && vitepress build",
+ "docs:dev": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && node bin/generate-llms-txt.mjs && vitepress dev",
+ "docs:build": "npm run build && npm run docs:api && node bin/build-catalog.mjs && node bin/harvest-plugins.mjs && node bin/generate-llms-txt.mjs && vitepress build",
"docs:preview": "vitepress preview",
"docs:push": "bash ./bin/push-docs.sh"
},
diff --git a/packages/tempo/public/bundle.index.html b/packages/tempo/public/bundle.index.html
index 3e9fa82d..14a726aa 100644
--- a/packages/tempo/public/bundle.index.html
+++ b/packages/tempo/public/bundle.index.html
@@ -99,6 +99,7 @@
margin-bottom: 5px;
background: linear-gradient(135deg, #fff, #a5b4fc);
-webkit-background-clip: text;
+ background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
diff --git a/packages/tempo/public/esm_core.index.html b/packages/tempo/public/esm_core.index.html
index 97e48f97..a1d060d6 100644
--- a/packages/tempo/public/esm_core.index.html
+++ b/packages/tempo/public/esm_core.index.html
@@ -99,6 +99,7 @@
margin-bottom: 5px;
background: linear-gradient(135deg, #fff, #a5b4fc);
-webkit-background-clip: text;
+ background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
diff --git a/packages/tempo/public/esm_full.index.html b/packages/tempo/public/esm_full.index.html
index 26b6fc93..514bfe61 100644
--- a/packages/tempo/public/esm_full.index.html
+++ b/packages/tempo/public/esm_full.index.html
@@ -99,6 +99,7 @@
margin-bottom: 5px;
background: linear-gradient(135deg, #fff, #a5b4fc);
-webkit-background-clip: text;
+ background-clip: text;
-webkit-text-fill-color: transparent;
letter-spacing: -0.5px;
}
diff --git a/packages/tempo/public/esm_sh.index.html b/packages/tempo/public/esm_sh.index.html
new file mode 100644
index 00000000..3d5576b0
--- /dev/null
+++ b/packages/tempo/public/esm_sh.index.html
@@ -0,0 +1,255 @@
+
+
+
+
+
+ Tempo esm.sh Test
+
+
+
+
+
+
+
+
+
+
Tempo
+
esm.sh Smart CDN Test
+
+
+ import'@js-temporal/polyfill';
+import { Tempo } from'@magmacomputing/tempo';
+
+const t = newTempo('next friday');
+t.format('{mon} {dd:raw}');
+
+
+
+ Result
+
Initializing Temporal...
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/tempo/public/library-logo.svg b/packages/tempo/public/library-logo.svg
index 55fb6bdf..ec7fd7c4 100644
--- a/packages/tempo/public/library-logo.svg
+++ b/packages/tempo/public/library-logo.svg
@@ -1,21 +1,34 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
diff --git a/packages/tempo/public/llms-full.txt b/packages/tempo/public/llms-full.txt
new file mode 100644
index 00000000..9bce34af
--- /dev/null
+++ b/packages/tempo/public/llms-full.txt
@@ -0,0 +1,7822 @@
+# Tempo Full Documentation Context
+
+> This file contains the complete concatenated markdown documentation set for @magmacomputing/tempo. It is intended for automated LLM context ingestion and RAG indexing.
+
+---
+
+
+
+
+# Document: 1-getting-started/ai-integration.md
+
+# 🤖 AI & IDE Integration (`llms.txt`)
+
+To ensure modern AI coding assistants—such as **Cursor**, **VS Code (GitHub Copilot)**, **Antigravity**, **ChatGPT**, and **Claude**—generate accurate, hallucination-free Tempo code, Tempo publishes an official, standardized [`llms.txt`](https://tempo.magmacomputing.com.au/llms.txt) rulebook.
+
+By providing these rules to your AI assistant, your IDE will respect Tempo's strict immutability, zero-cost getters, native `Temporal` runtime expectations, and layout token syntax out-of-the-box.
+
+---
+
+## 🚀 Quick Setup by IDE / Tool
+
+### 1. Cursor IDE
+Add Tempo to Cursor's native documentation index:
+1. Open **Cursor Settings** (`Cmd + ,` or `Ctrl + ,`).
+2. Navigate to **Features** ➔ **Docs**.
+3. Click **+ Add new doc** and enter:
+ - **Name**: `Tempo`
+ - **URL**: `https://tempo.magmacomputing.com.au/llms.txt`
+
+> [!TIP]
+> Once added, type `@Tempo` in any Cursor chat or prompt window to inject exact API syntax rules into your conversation.
+
+---
+
+### 2. VS Code & GitHub Copilot
+In VS Code, configure GitHub Copilot Chat by adding a `.github/copilot-instructions.md` file (or `.cursorrules`) to the root of your workspace:
+
+```markdown
+# Tempo AI Rules
+- Always use `Tempo` from `@magmacomputing/tempo`.
+- Never instantiate legacy JavaScript `Date`. Tempo expects native `Temporal` or polyfill.
+- All mutating methods (`.add()`, `.subtract()`, `.with()`) return a brand-new, frozen `Tempo` instance.
+- Refer to https://tempo.magmacomputing.com.au/llms.txt for full layout token grammar.
+```
+
+When prompting Copilot Chat in VS Code:
+```text
+"Using https://tempo.magmacomputing.com.au/llms.txt, write a custom layout parser..."
+```
+
+---
+
+### 3. Antigravity AI Assistant
+In Antigravity, you can reference the live endpoint directly in your chat prompt or store it as a localized Knowledge Item (KI):
+- Reference `@https://tempo.magmacomputing.com.au/llms.txt` in your prompt for instant context ingestion.
+
+---
+
+### 4. ChatGPT & Claude Projects
+For web-based LLM interfaces, reference or copy-paste the full, un-truncated documentation context file:
+👉 **[Full RAG Documentation Bundle (`llms-full.txt`)](https://tempo.magmacomputing.com.au/llms-full.txt)**
+
+---
+
+## 🛠️ Prompting AI for Custom Layout Extensions
+
+When asking AI assistants to generate custom layout patterns for parsing unique date-time formats, instruct the model to use Tempo's configuration syntax (`Tempo.config({ layouts: { ... } })`) and named capture tokens (`{yy}`, `{mon}`, `{dd}`, `{hh}`, `{mi}`, `{ss}`, `{tzd}`).
+
+### Sample Prompt:
+> *"Using the rules from https://tempo.magmacomputing.com.au/llms.txt, register a custom Tempo layout for fiscal quarters (e.g., 'Q3 2026') using `Tempo.config({ layouts: { ... } })` and parse a date using `Tempo.parse()`."*
+
+### Generated Code (Actual Tempo Syntax):
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+
+// 1. Register custom layout pattern using snippet tokens
+Tempo.config({
+ layouts: {
+ fiscal_quarter: 'Q{nbr} {yy}'
+ }
+});
+
+// 2. Parse date string using the registered layout
+const date = Tempo.parse('Q3 2026', 'fiscal_quarter');
+```
+
+
+
+
+---
+
+
+
+# Document: 1-getting-started/installation.md
+
+# Installation Guide
+
+`Tempo` is designed to be environment-agnostic. Whether you are building a server-side application, a modern browser project with ESM, or a performance-critical "Lite" bundle, `Tempo` provides a specific path for you.
+
+## Temporal Polyfill Note
+
+`Tempo` expects the host environment to provide `Temporal`, either through native runtime support or a user-supplied polyfill.
+
+`Temporal` has reached Stage 4 of the [TC39 standards process](https://tc39.es/proposal-temporal/) (the committee that evolves JavaScript) and is shipping natively in modern environments (Deno 2.7+, Node.js 26+, Chrome 144+, Firefox 139+). Note that Safari/iOS currently do not support Temporal natively and require a polyfill. You can verify current browser support at [caniuse.com/temporal](https://caniuse.com/temporal). To avoid needlessly inflating package sizes for modern apps, `Tempo` does not bundle a `Temporal` polyfill by default.
+
+::: warning
+Node.js environments that ship `Temporal` behind a feature flag (`--harmony-temporal`) may have incomplete implementations. For stability, we strongly recommend using `@js-temporal/polyfill` instead of the native flag until you upgrade to an official unflagged release.
+:::
+
+You can check at runtime with a simple guard:
+
+```js
+if (typeof globalThis.Temporal === 'undefined') {
+ // Load your Temporal polyfill for this environment
+}
+```
+
+Note: The examples below include a polyfill for demonstration purposes only, so the snippets work consistently across environments.
+
+---
+
+## 💻 Server & Bundlers (Node.js, Bun, Vite)
+
+For most modern projects using a package manager, install Tempo via the npm registry.
+
+```bash
+npm install @magmacomputing/tempo # npm
+yarn add @magmacomputing/tempo # yarn
+pnpm add @magmacomputing/tempo # pnpm
+bun add @magmacomputing/tempo # bun
+```
+
+### Usage
+```javascript
+import { Tempo } from '@magmacomputing/tempo';
+const t = new Tempo('next Friday');
+```
+
+### Node.js (with Native Temporal)
+
+Native unflagged `Temporal` support is available in Node.js 26+ and is enabled by default.
+
+```bash
+node my-app.js
+```
+
+### Node.js (with Polyfill)
+
+The polyfill import shown here is conditional guidance, not required for all environments.
+
+```bash
+npm install @js-temporal/polyfill
+```
+
+```javascript
+import '@js-temporal/polyfill';
+import { Tempo } from '@magmacomputing/tempo';
+
+const t = new Tempo('next Friday');
+```
+
+---
+
+## 🦕 Deno
+
+Tempo is a native ESM package and works perfectly with Deno. You can add it via the `deno add` command which will resolve it from the npm registry.
+
+As of Deno 2.7, the Temporal API is fully stabilized and enabled by default. You no longer need to pass the --unstable-temporal flag to use it.
+
+```bash
+deno add npm:@magmacomputing/tempo
+```
+
+### Usage
+```javascript
+import { Tempo } from "@magmacomputing/tempo";
+const t = new Tempo();
+```
+
+---
+
+## 🌐 Browser & Native Environments
+
+Tempo provides multiple native browser distribution formats. Here is the quick breakdown of which approach to use:
+- **Standard Usage** (No plugins): Use the Native ESM Bundle.
+- **Plugins without a bundler**: Use **Smart CDNs** (Easiest setup) OR **Static CDNs** (Best production performance).
+- **Plugins with a bundler** (Vite/Webpack): Do nothing. Your bundler handles the resolution automatically.
+- **Non-ESM Environments**: Use the UMD Global Variable approach.
+
+### 1. The Global Bundle (Standard Usage)
+
+The easiest way to use Tempo natively in the browser is via the pre-optimized ESM bundle. It includes the entire core engine in a single file, eliminating network waterfall effects.
+
+```html
+
+```
+
+```html
+
+```
+
+### 2. Smart CDNs (The "Best-of-Both-Worlds")
+
+If you want the absolute easiest setup for **Tempo Plugins** natively in the browser, use an on-the-fly bundling CDN like [esm.sh](https://esm.sh). Smart CDNs act like a Node environment—they read the package resolution rules and resolve nested dependencies automatically, meaning you don't have to map any internal subpaths.
+
+While you *could* import directly from the URL everywhere, the best practice is to use a tiny import map for your top-level packages to keep your application code clean:
+
+```html
+
+
+
+
+
+```
+
+
+⚠️ Trade-offs of using Smart CDNs in Production
+
+While `esm.sh` is fantastic for prototyping and reducing import map complexity, there are architectural trade-offs to consider before using it in a mission-critical production environment:
+
+1. **Network Waterfalls:** The browser must fetch the module, parse it, and then fetch its nested dependencies sequentially. This can slow down page load times compared to a fully bundled application.
+2. **Uptime Dependency:** You are introducing a critical third-party dependency into your runtime. If the CDN experiences routing issues, your application could break for end-users.
+3. **Sub-dependency Version Floating:** `esm.sh` automatically resolves sub-dependencies based on semver constraints. If a sub-dependency introduces an accidental breaking change, it could affect your app.
+4. **Suboptimal Tree Shaking:** The browser will download the entire module graph for that package; you cannot easily tree-shake unused exports as you can with a dedicated bundler like Vite or Webpack.
+5. **Environment Parity:** Handling development versus production environments (like `process.env.NODE_ENV`) requires query parameters (e.g., `?dev`), which complicates deployment.
+
+
+
+### 3. Static CDNs (Production-Ready)
+
+For production environments where uptime and load speeds are critical, you should use a static file CDN (like jsdelivr). Because static CDNs serve raw files without compiling them on the fly, they are significantly faster and more reliable than Smart CDNs.
+
+To use **Tempo Plugins** via a static CDN, you simply need to explicitly map the unified `plugin-api` subpath so the browser knows how to resolve the internal connections:
+
+```html
+
+```
+
+> [!WARNING] Cache Busting
+> The jsdelivr CDN aggressively caches major version tags (like `@3`). When relying on precise module resolution for plugins, it is highly recommended to use explicit patch versions (like `@3.0.1`) to avoid fetching mismatched or outdated sub-modules.
+
+---
+
+## 📦 Browser (Global Variable / Plugins)
+
+If you aren't using ESM or just want a simple `
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 🧪 Granular "Lite" Builds (Advanced)
+
+If you are extremely concerned about bundle size, you can bypass the "Batteries Included" entry point and import only the core engine. You then manually opt-in to the modules you need.
+
+```javascript
+import { Tempo } from '@magmacomputing/tempo/core';
+import { MutateModule } from '@magmacomputing/tempo/mutate';
+
+// Opt-in to specific functionality
+Tempo.extend(MutateModule);
+
+const t = new Tempo().add({ days: 1 });
+```
+
+::: warning
+When using the Lite build, the `Tempo` class will have almost no methods (like `.add()`, `.set()`, or `.format()`) until you explicitly call `Tempo.extend()` with the appropriate module.
+:::
+
+---
+
+## 🛡️ Versioning Policy
+
+We recommend pinning your versions in production environments to ensure stability.
+
+* **JSDelivr**: `https://cdn.jsdelivr.net/npm/@magmacomputing/tempo@3/...` (Locks to major version 3)
+* **Latest**: `https://cdn.jsdelivr.net/npm/@magmacomputing/tempo/...` (Omit the version string to always receive the latest release. Note that JSDelivr will resolve a missing version tag to the latest published release).
+
+---
+
+## 🤖 AI & IDE Integration (`llms.txt`)
+
+> [!TIP]
+> Using **Cursor**, **VS Code (GitHub Copilot)**, **Antigravity**, **ChatGPT**, or **Claude**?
+> Tempo publishes an official [`llms.txt`](https://tempo.magmacomputing.com.au/llms.txt) index to give AI assistants zero-hallucination context about Tempo's syntax, token grammar, and immutability rules.
+>
+> 👉 **[Read the dedicated AI & IDE Integration Guide](./ai-integration.md)** for step-by-step setup instructions for your IDE or tool.
+
+
+
+
+
+---
+
+
+
+# Document: 1-getting-started/tempo.cookbook.md
+
+# Tempo Cookbook
+
+A collection of recipes for solving common date and time challenges using Tempo.
+
+## Table of Contents
+1. [The Basics](#the-basics)
+2. [Parsing Challenges](#parsing-challenges)
+3. [Manipulation and Calculations](#manipulation-and-calculations)
+4. [Timezones and Locales](#timezones-and-locales)
+5. [Business Logic and Terms](#business-logic-and-terms)
+6. [Formatting and Localization](#formatting-and-localization)
+7. [Interoperability](#interoperability)
+
+---
+
+## The Basics
+
+### How do I get the current date and time?
+When invoked without arguments, the constructor initializes to the current date and time.
+```typescript
+const now = new Tempo();
+console.log(now.toString()); // e.g. "2026-07-31T14:42:11+10:00[Australia/Sydney]"
+```
+
+### Get "Now" in UTC
+```typescript
+const utcNow = new Tempo({ timeZone: 'UTC' });
+```
+
+### How do I format a date for my UI?
+Use the placeholder syntax in the `.format()` method.
+```typescript
+const t = new Tempo('2024-12-25');
+t.format('{dd} {mon} {yyyy}'); // "25 December 2024"
+t.format('{h12}:{mi}'); // "12:00am"
+```
+
+### How do I check if a date is valid?
+```typescript
+const t = new Tempo('invalid-date');
+if (t.isValid) {
+ // ...
+}
+```
+
+### Global Configuration
+You can initialize global defaults that apply to all future `Tempo` instances.
+```typescript
+Tempo.init({
+ timeZone: 'UTC',
+ locale: 'en-GB',
+ silent: true
+});
+```
+👉 **Learn More:** [Configuration Guide](../2-core-concepts/tempo.config.md)
+
+---
+
+## Parsing Challenges
+
+### Parsing "Ambiguous" Digits (US vs UK)
+Tempo intelligently resolves ambiguous dates like `04012026` based on your timezone.
+```typescript
+const us = new Tempo('04012026', { timeZone: 'America/New_York' });
+console.log(us.format('{mon} {dd}')); // "April 01"
+```
+👉 **Learn More:** [Ambiguity Resolution Guide](../2-core-concepts/tempo.parse.md)
+
+### Handling Relative Strings
+Tempo natively understands human-readable offsets.
+```typescript
+new Tempo('yesterday');
+new Tempo('next Friday');
+new Tempo('2 weeks ago');
+new Tempo('tomorrow afternoon');
+```
+
+👉 **Learn More:** You can seamlessly localize relative phrases (e.g. `next` to `prochain`) by reading the [Internationalized Parsing Guide](../2-core-concepts/tempo.parse.md#internationalized-parsing-locales).
+
+### Parsing Unix Timestamps
+Tempo handles both milliseconds (Number) and nanoseconds (BigInt).
+```typescript
+new Tempo(1716163200000); // Milliseconds
+new Tempo(1716163200000000000n); // Nanoseconds
+```
+
+---
+
+## Manipulation and Calculations
+
+### Add or Subtract Time
+Tempo instances are immutable; `add()` returns a new instance.
+```typescript
+const deadline = new Tempo().add({ days: 7, hours: 2 });
+const past = new Tempo().add({ months: -1 });
+
+// You can also step by semantic Terms using the `#` prefix!
+const t1 = new Tempo('2024-05-15'); // Middle of Q2
+const t2 = t1.add({ '#quarter': 1 }); // Middle of Q3: "2024-08-14" (approx)
+```
+
+### Jumping to Boundaries (`start`, `mid`, `end`)
+The `.set()` method allows you to jump to the boundaries of native units (like months or years) or semantic Terms (using the `#` prefix). You can specify whether to land on the inclusive start, inclusive end, or the exact center.
+```typescript
+// Native Units
+const monthStart = new Tempo().set({ start: 'month' });
+
+// Semantic Terms (Lands on 30-Sep 23:59:59.999... Inclusive End)
+const qtrEnd = new Tempo().set({ end: '#quarter' });
+
+// Lands on the arithmetic nanosecond midpoint of the period
+const qtrMid = new Tempo().set({ mid: '#quarter' });
+```
+
+### Slick Object Mutations
+You can navigate relative to your current date by using Slick Shorthand operators directly inside `.set()`. Use the snippet shorthand keys (`yy`, `mm`, `ww`, `dd`, `wkd`, etc.) and provide a string payload containing a directional modifier:
+
+```typescript
+const t = new Tempo('2024-05-20'); // Monday
+t.set({ mm: '>2' }); // July 20th
+t.set({ wkd: '>Fri' }); // May 24th
+```
+
+👉 **Learn More:** To read about advanced chaining, order-of-operations, and architectural limitations, see the [Slick Object Mutations Deep Dive](../2-core-concepts/tempo.mutate.md#slick-object-mutations).
+
+### How long until a deadline? (`until`)
+```typescript
+const t = new Tempo();
+const daysLeft = t.until('2025-01-01', 'days');
+console.log(`${daysLeft} days remaining`);
+```
+
+### Relative Time (`since`)
+Generate human-readable relative time strings instantly.
+```typescript
+const t = new Tempo('yesterday');
+console.log(t.since()); // "1d ago"
+```
+
+---
+
+## Timezones and Locales
+
+### Convert Time to Another Zone
+```typescript
+const nyc = new Tempo('2024-05-20 10:00', { timeZone: 'America/New_York' });
+const london = nyc.set({ timeZone: 'Europe/London' });
+
+console.log(nyc.format('{hh}:{mi}')); // "10:00"
+console.log(london.format('{hh}:{mi}')); // "15:00"
+```
+
+---
+
+## Business Logic and Terms
+
+### Is it the weekend?
+```typescript
+const t = new Tempo();
+const isWeekend = t.dow >= 6; // Saturday = 6, Sunday = 7
+```
+
+### What Fiscal Quarter are we in?
+Using the `qtr` Term plugin (`term.qtr` is a convenient alias for the full `term.quarter` property).
+```typescript
+const t = new Tempo();
+console.log(`Current Quarter: ${t.term.qtr}`); // "Q1", "Q2", etc.
+```
+
+### Hemispheric Seasons
+Tempo Terms are hemisphere-aware.
+```typescript
+const sydney = new Tempo('2024-07-01', { sphere: 'south' });
+console.log(sydney.term.szn); // "Winter"
+
+const london = new Tempo('2024-07-01', { sphere: 'north' });
+console.log(london.term.szn); // "Summer"
+
+// or even via the timeZone setting
+console.log(new Tempo({ timeZone: 'America/New_York' }).term.szn); // "Summer"
+console.log(new Tempo({ timeZone: 'Australia/Sydney' }).term.szn); // "Winter"
+```
+
+---
+
+## Formatting and Localization
+
+### Semantic Formatting
+Use specific Term tokens like `{#quarter}` or `{#season}` to automatically embed a Term's label (or key) into a format string.
+```typescript
+const t = new Tempo();
+console.log(t.format('We are currently in the {#quarter}')); // "We are currently in the First Quarter"
+```
+
+### Format Modifiers & Localization
+Format strings support chained colon-modifiers (e.g., `:upper`, `:locale`, `:ord`) to dynamically change the presentation casing or delegate to the native `Intl` API. You can stack them to get the exact presentation required!
+
+```typescript
+const t = new Tempo('2024-05-15 15:30', { locale: 'fr-FR' });
+
+t.format('{mon:upper}'); // "MAY" (English Default -> UpperCase)
+t.format('{mon:long}'); // "mai" (Native French Intl output via styling bridge)
+t.format('{mon:long:upper} {dd}'); // "MAI 15" (Native French Intl output)
+```
+
+👉 **Learn More:** See the [Smart Formatting Guide](../2-core-concepts/tempo.format.md) for the complete list of available modifiers.
+
+::: tip
+**Tired of typing styling modifiers?**
+If you find yourself repeatedly writing `:long` or `:short` for the same localized date structure, save it to the global **FORMATS** registry! This creates a clean, reusable shortcut:
+```typescript
+Tempo.init({
+ locale: 'fr-FR',
+ registry: {
+ formats: {
+ 'ui-date': '{wkd:long}, {dd:raw} {mon:long} {yyyy}'
+ }
+ }
+});
+
+t.format('ui-date'); // Resolved with all modifiers intact!
+```
+
+*Note: Format keys are resolved case-sensitively from the global `registry.formats` object. If the requested key is not found, Tempo will simply treat the provided string as a literal layout string rather than throwing an error.*
+:::
+
+👉 **Learn More:** To build custom zero-overhead logic evaluators (like Fiscal Years or native Intl bridges), read the [Custom Format Tokens Deep Dive](../2-core-concepts/tempo.format.md#custom-format-tokens).
+
+👉 **Learn More:**
+- [Smart Formatting Guide](../2-core-concepts/tempo.format.md)
+- [The Role of Locale](../4-advanced-reference/tempo.locale.md)
+- [Smart Parsing Guide](../2-core-concepts/tempo.parse.md)
+
+---
+
+### Ticker Plugin
+The Ticker engine is a premium plugin for precisely driving business logic (like recurring billing or reporting cycles) on specific date boundaries.
+
+```typescript
+// Drive internal reporting precisely when a new quarter begins
+await using quarterly = Tempo.ticker({ '#quarter': 1 });
+
+for await (const t of quarterly) {
+ generateReport(t.term.qtr);
+}
+```
+
+👉 **Learn More:** See the [Ticker Plugin Documentation](../../../plugins/ticker/doc/index.md) for detailed configuration, term-driven intervals, and `await using` syntax requirements.
+
+
+---
+
+## Interoperability
+
+### Converting to / from Native `Date`
+```typescript
+const date = new Tempo().toDate();
+const tempo = new Tempo(new Date());
+```
+
+### Converting to `Temporal` Objects
+```typescript
+const zdt = new Tempo().toDateTime(); // Temporal.ZonedDateTime
+const instant = new Tempo().toInstant(); // Temporal.Instant
+const pdt = new Tempo().toPlainDate(); // Temporal.PlainDate
+```
+
+### Sorting an array of Tempos
+```typescript
+const dates = [new Tempo('tomorrow'), new Tempo('yesterday'), new Tempo('today')];
+dates.sort(Tempo.compare); // Sorts chronologically
+```
+
+
+
+
+
+---
+
+
+
+# Document: 2-core-concepts/tempo.cache.md
+
+# Cache Management Guide
+
+**Tempo** includes a centralized, high-performance **`BoundedCache`** singleton accessible via `Tempo.cache`. It provides dual-layer resolution for dynamic relative dates (with LRU eviction and TTL expiration) and static business glossaries (immortal keys).
+
+---
+
+## 🏛️ Centralized Cache Architecture
+
+All date resolution caching—whether triggered by core `Tempo` parsing or the Tempo AI plugin (`parseAI`, `formatAI`, `contextAI`)—is managed centrally by `Tempo.cache`.
+
+::: info Cache Behavior: Core Tempo vs. Tempo AI Plugin
+* **Core Tempo**: Caching is **opt-in**. Core date parsing executes at sub-microsecond speeds using standard regex matching. `Tempo.cache` is consulted when you seed a static glossary or enable caching.
+* **Tempo AI Plugin**: Caching is **automatic**. To eliminate network latency (~500ms+) and avoid redundant LLM API billing, AI functions (like `parseAI`) automatically check `Tempo.cache` before sending network requests and cache every successful LLM resolution.
+:::
+
+### Cache Topology & Configuration
+
+You can configure global cache parameters using `Tempo.init()`:
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+
+Tempo.init({
+ cache: {
+ maxSize: 1000, // Maximum number of entries before LRU eviction (default: 1000)
+ ttl: 24 * 60 * 60 * 1000 // Time-to-live in milliseconds (default: 24 hours)
+ }
+});
+```
+
+* **Capacity Management (LRU):** When the cache reaches `maxSize`, the Least Recently Used dynamic entry is automatically evicted.
+* **TTL Expiration:** Dynamic entries older than `ttl` are automatically purged upon lookup.
+* **Static Glossary Isolation:** Static entries added to the glossary are **exempt** from both LRU eviction and TTL expiration.
+
+---
+
+## 📖 Seeding & Appending Glossaries
+
+You can seed static business terms into `Tempo.cache` using a native JavaScript `Map` or via `Tempo.init({ cache: map })`:
+
+```typescript
+const businessGlossary = new Map([
+ ['fiscal year start 2026', '2026-07-01T00:00:00Z'],
+ ['q3 board review', '2026-09-15T09:00:00Z']
+]);
+
+// Appends entries to Tempo.cache as static, immortal terms
+Tempo.init({ cache: businessGlossary });
+```
+
+::: tip Non-Destructive Appending
+Passing a `Map` or custom key-value pairs to `Tempo.init({ cache })` or `initAI({ cache })` **appends** to the existing cache without clearing previously cached terms or resetting cache capacity settings.
+:::
+
+---
+
+## 💡 When to Use What: Glossary vs. Alias vs. Snippet/Layout
+
+Tempo provides multiple mechanisms for augmenting parsing intelligence. Choosing the right pattern depends on whether your logic is static, dynamic, structural, or string replacement:
+
+| Mechanism | Tier / Location | Evaluation Model | Best Used For... |
+| :--- | :--- | :--- | :--- |
+| **Glossary** (`Tempo.cache`) | Core Engine | Zero-cost `O(1)` Map lookup | Pre-calculated static ISO date/time strings or exact business dates. |
+| **Aliases / Events / Periods** (`registry.events` / `periods`) | Registry Engine | Dynamic function or target string | Computing dynamic business dates (e.g. `'deadline' => () => this.add({ days: 30 })`). |
+| **Snippet / Layouts** (`registry.snippets` / `layouts`) | Parser Planner | Regex pattern matcher | Structural natural language formats (e.g. `yyyy/mm/dd` or custom date tokens). |
+
+### Decision Tree
+
+1. **Use a Glossary (`Tempo.cache`)** when you have fixed, pre-resolved ISO dates for specific terms (e.g., `'eoy 2026'` -> `'2026-12-31T23:59:59Z'`). It offers instant `O(1)` resolution without invoking the regex parser.
+2. **Use an Alias (`registry.events` / `periods`)** when you need dynamic rules calculated relative to the current date/time (e.g., `'market-close'` -> `'16:00'` or `'deadline'` -> `30 days from now`).
+3. **Use a Snippet or Layout (`registry.snippets` / `layouts`)** when parsing custom input structures with variable numbers or tokens (e.g. `"2026-W05"` or `"Quarter 3, 2026"`).
+
+---
+
+## 🤖 AI Plugin Cache Integration (`@magmacomputing/tempo-plugin-ai`)
+
+The `@magmacomputing/tempo-plugin-ai` plugin works hand-in-hand with `Tempo.cache` to reduce LLM API calls and costs across AI functions:
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+import { parseAI, initAI } from '@magmacomputing/tempo-plugin-ai';
+
+initAI({ providers: [...] });
+
+// First lookup: Triggers LLM call -> Stores ISO result in Tempo.cache
+const t1 = await parseAI("The penultimate Tuesday before Thanksgiving in 2026");
+
+// Second lookup: Instantly resolves from Tempo.cache (O(1) local hit, $0 cost)
+const t2 = new Tempo("The penultimate Tuesday before Thanksgiving in 2026");
+```
+
+### Two-Tier Resolution Architecture
+1. **Date-Salted Relative Cache**: Relative queries (e.g. `"next Tuesday"`) are salted with the anchor date so cached entries remain valid for the given day.
+2. **Static Glossary Fallback**: Business glossary terms seeded via `initAI({ cache })` or `Tempo.init({ cache })` are checked first, providing zero-latency resolution without ever contacting the LLM.
+
+
+
+
+---
+
+
+
+# Document: 2-core-concepts/tempo.config.md
+
+# Configuration Guide
+
+**Tempo** provides a flexible, multi-tiered configuration system. Settings are applied in a specific order of precedence, allowing you to set broad defaults that can be refined at the application or instance level.
+
+## Precedence Hierarchy
+
+Settings are loaded in the following order (where later stages override earlier ones):
+1. **Library Defaults**: Sensible out-of-the-box baseline.
+2. **Persistent Storage**: Sticky user preferences (which merge into Defaults).
+3. **Global Discovery**: Enterprise-level setup discovered via `Symbol.for('$Tempo')`.
+4. **Library Extension**: Dynamic feature registration via `Tempo.extend()`.
+5. **Explicit Initialization**: Baseline configuration via `Tempo.init()`.
+6. **Instance Constructor**: Specific overrides for a single `new Tempo()` call.
+
+---
+
+## 🏆 Best Practice: The `tempo.config.ts` Pattern
+
+Rather than scattering `Tempo.init()` or `Tempo.extend()` calls throughout your application, the recommended best practice is to centralize your environment setup into a single `tempo.config.ts` (or `.js`) file.
+
+This mirrors modern ecosystem standards (like `vite.config.ts` or `tailwind.config.js`) and ensures that plugins, timezones, and custom aliases are consistently applied before any domain logic executes.
+
+::: info
+**Target Environment**: This automatic configuration discovery pattern relies on Node.js file system capabilities and is designed for Server, Fullstack, or Bundled environments (like Vite or Webpack). If you are using Tempo via a `
+
+
+```
+
+#### 2. Frontend Bundlers without `process.env` Polyfills
+Modern browser bundlers (e.g., Vite) do not inject Node's `process` object by default. If you prefer to avoid configuring build-time env replacements or `dotenv` plugins, assign the key to `globalThis` in your entry file and use dynamic imports to ensure the key is set before Tempo initializes:
+
+```javascript
+// entry.js
+globalThis.TEMPO_LICENSE_KEY = import.meta.env.VITE_TEMPO_LICENSE_KEY;
+
+// Use dynamic imports so the key is set before Tempo's static initializer runs
+const { Tempo } = await import('@magmacomputing/tempo');
+const { TickerPlugin } = await import('@magmacomputing/tempo-plugin-ticker');
+
+Tempo.init({ plugins: [TickerPlugin] });
+```
+
+Alternatively, pass the license key explicitly via `Tempo.init()` after your static imports:
+
+```javascript
+import { Tempo } from '@magmacomputing/tempo';
+import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker';
+
+Tempo.init({
+ license: import.meta.env.VITE_TEMPO_LICENSE_KEY,
+ plugins: [TickerPlugin]
+});
+```
+
+#### 3. Micro-frontends / Shared Global Space
+In architectures where multiple independently-bundled applications share a single browser tab, set the key once in the host container. All dynamically-loaded sub-applications will then auto-discover it without needing individual configuration:
+
+```javascript
+// host-container.js
+globalThis.TEMPO_LICENSE_KEY = 'eyJhbGciOiJSUzI1NiJ9...';
+
+// sub-apps loaded later will automatically run in licensed mode
+```
+
+## 4. Network Requests & Offline Behavior
+
+To verify license validity and prevent abuse, Tempo's licensing engine performs background synchronization with our revocation registry:
+
+* **Outbound Request:** When a license key is active, Tempo asynchronously fetches a cryptographically signed revocation list (JWS).
+* **Endpoint:** `https://registry.magmacomputing.com.au/tempo/v1/revoked.jws` (useful for configuring Content Security Policies (CSP) or egress firewall rules).
+* **Frequency:** The revocation check occurs once every **7 days**. The last-checked state is cached to avoid redundant network traffic on subsequent startups.
+* **Offline Resilience (Fail-Open):** If your application is offline, behind a strict firewall, or the registry server is temporarily unreachable, the validation **fails open**. Tempo emits a debug-level log entry but continues to grant access to premium features (relying on the local cryptographic expiration of the JWT).
+
+## 5. Commercialize Your Own Plugin
+
+Are you a developer who has built an incredibly useful, domain-specific Tempo plugin (e.g., medical billing cycles, legal discovery windows, complex religious calendars)?
+
+If you would like to monetize your logic without having to build your own licensing infrastructure, **we want to partner with you**.
+
+Get in touch with us with your proposed code and use-case. If it meets our quality and performance standards, we can publish it as an official Premium Extension secured behind the Tempo License Key system, under a mutually beneficial commercial revenue-sharing arrangement.
+
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/ai.architecture.md
+
+# Provider Architecture & Security
+
+The `@magmacomputing/tempo-plugin-ai` plugin is designed to be highly flexible, supporting both direct Bring Your Own Key (BYOK) integrations for backend systems, and Proxied integrations for frontend clients.
+
+## Bring Your Own Key (BYOK)
+
+For Node.js backends and Edge Workers, the simplest approach is to supply your raw API keys directly to the `initAI` function.
+
+```typescript
+import { initAI } from '@magmacomputing/tempo-plugin-ai';
+
+initAI({
+ providers: [
+ ...(process.env.GROQ_API_KEY ? [{ id: 'groq', key: process.env.GROQ_API_KEY }] : []),
+ ...(process.env.GEMINI_API_KEY ? [{ id: 'gemini', key: process.env.GEMINI_API_KEY }] : []),
+ ...(process.env.OPENAI_API_KEY ? [{ id: 'openai', key: process.env.OPENAI_API_KEY }] : [])
+ ]
+});
+```
+
+### Advanced Configuration (Custom Models & LLM Options)
+By default, standard providers automatically map to their optimal APIs and default models.
+However, you can explicitly override URLs, models, and inject arbitrary LLM parameters (like `temperature`) for power-user control!
+
+```typescript
+initAI({
+ providers: [
+ // 1. Enterprise Azure OpenAI (via Entra ID Bearer token or backend proxy wrapper)
+ // Note: BYOK requests send 'Authorization: Bearer '. When connecting to Azure OpenAI,
+ // supply an Entra ID bearer token as provider.key or route through an Azure API gateway.
+ ...(process.env.AZURE_ENTRA_BEARER_TOKEN ? [{
+ id: 'openai',
+ key: process.env.AZURE_ENTRA_BEARER_TOKEN,
+ url: 'https://my-enterprise.openai.azure.com/v1/chat/completions',
+ model: 'your-enterprise-model',
+ options: { temperature: 0.2, seed: 42 }
+ }] : []),
+ // 2. Local Open-Source Models (e.g. Ollama)
+ {
+ id: 'local',
+ key: 'no-key-needed',
+ url: 'http://localhost:11434/v1/chat/completions',
+ model: 'your-local-model',
+ options: { timeout: 5000 } // Custom provider-level timeout (5s)
+ }
+ ]
+});
+```
+
+### Frontend Security Warning
+> [!CAUTION]
+> **Never** expose a raw LLM API key in a client-side browser bundle (like React or Vue) or store it in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). Any Cross-Site Scripting (XSS) vulnerability, compromised NPM package, or malicious browser extension can easily inspect client-side storage and steal secret keys, leading to quota exhaustion, billing fraud, or permanent provider bans. BYOK keys are *only* safe on backend servers or edge workers.
+
+## The Proxy Architecture
+
+If you need to execute AI functions directly on a public frontend application, you must route requests through a secure backend proxy.
+
+A standard proxy architecture (e.g. using Cloudflare Workers or a custom Node/Express backend) involves:
+1. **Frontend Request**: The browser sends the prompt or temporal data to your own backend API (e.g., `/api/parse-date`).
+2. **Backend Authentication**: Your API validates the user's session or API token to prevent abuse.
+3. **LLM Inference**: Your backend runs the Tempo AI function (such as `parseAI`) using your securely stored BYOK keys.
+4. **Response**: Your backend returns the resulting ISO 8601 string to the frontend, where it can be instantiated into a native `Tempo` object.
+
+Because LLM API calls typically take ~300-800ms, the ~20ms overhead of routing the request through your own backend proxy is negligible.
+
+## Fallback Loops & Execution Modes
+
+Because third-party APIs can experience downtime or aggressive rate limiting, the plugin supports flexible multi-provider execution strategies:
+
+### 1. Fallback Mode (Default)
+When configured with multiple providers in `initAI()`, AI functions execute requests sequentially. If the primary provider hits a timeout or a `429 Too Many Requests` limit, the plugin instantly and silently fails over to the next provider in the array. Rate limit headers are updated based on the successful provider response or error resolution.
+
+### 2. Race Mode (`mode: 'race'`)
+Dispatches requests to all available providers simultaneously using `Promise.allSettled`. Returns the fastest resolving provider response to minimize user-perceived latency.
+
+```typescript
+const result = await parseAI("Thanksgiving 2026", { mode: 'race' });
+```
+
+### 3. Consensus Mode (`mode: 'consensus'`)
+Executes all providers concurrently. If multiple providers agree on the resolved ISO timestamp, confidence score is boosted (to `1.0`) and the consensus result is returned. Rate limits are applied from the consensus provider.
+
+```typescript
+const result = await parseAI("The penultimate Tuesday before Thanksgiving", {
+ mode: 'consensus',
+ minConfidence: 0.85
+});
+```
+
+### Provider ID Canonicalization
+Provider IDs are normalized case-insensitively during `initAI` lookup (e.g. `'Gemini'`, `'gemini'`, `'OpenAI'`), automatically applying default endpoints and models while preserving the caller's registered identifier for logging and metadata.
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/ai.context.md
+
+# Context & Natural Language Parsing
+
+Because natural language dates are entirely relative (e.g., "next Tuesday") and often geographically ambiguous (e.g., "11/12"), an LLM cannot reliably parse them in a vacuum.
+
+The Tempo AI plugin solves this by automatically wrapping your input with rich environmental context before sending it to the LLM.
+
+## Geographic Context
+
+The plugin automatically reads from the global `Tempo.config` to fetch the default TimeZone, Calendar, and Locale, and establishes the "current anchor time" the moment you call it.
+
+Along with your string, the plugin passes a hidden context payload to the LLM:
+*`Current Time: [Anchor], Timezone: [TZ], Calendar: [Cal], Locale: [Locale], Hemisphere: [Sphere]`*
+
+### Overriding Context
+You can explicitly override any of these global settings on a per-request basis by passing an `options` object as the second argument, identical to how you pass options to a standard `new Tempo()` constructor:
+
+```typescript
+// Explicitly evaluate this complex query from the perspective of September 1st
+const dt = await parseAI("The penultimate Tuesday before Thanksgiving", { anchor: '2026-09-01T00:00:00Z' });
+
+// Explicitly parse assuming a Japanese locale and timezone
+const tokyoDt = await parseAI("The second Sunday of May", { locale: 'ja-JP', timeZone: 'Asia/Tokyo' });
+```
+
+### Why Locale is Critical
+Passing the `Locale` is absolutely critical for the LLM to know whether "11/12" means November 12th (US format) or 11th of December (UK/EU format). The plugin handles this transparently based on your standard Tempo configuration!
+
+> [!WARNING]
+> **Calendar Math Hallucinations**: LLMs are language predictors, not calculators. While they excel at parsing conversational times (like `"tomorrow at 5pm"`), smaller models are notoriously prone to hallucinations on complex, cross-year calendar math. For example, asking a lightweight model for `"Thanksgiving in 2026"` may result in a hallucinated day of the week because the model doesn't natively compute "the fourth Thursday of November 2026." If your application relies on heavy holiday logic or complex multi-year math, you *must* use a capable frontier model or rely on deterministic plugins instead of AI.
+
+## The Decoupled Output Bridge
+
+To ensure deterministic behavior, the LLM is instructed to *only* return strict ISO 8601 strings.
+
+The plugin executes the network request, the LLM returns a local ISO string without a timezone offset or 'Z' suffix (like `"2026-11-26T00:00:00"`), and the plugin immediately passes that string back into the native `new Tempo()` constructor. The provider response must omit timezone suffixes to match the local ISO contract enforced by Tempo AI functions. The developer seamlessly receives a valid, native `Tempo` instance. This eliminates AST-construction ambiguity, creating a decoupled bridge between AI text generation and native Tempo conversion.
+
+### Relative Date Ambiguity Tie-Breakers
+
+To eliminate model variance on idioms like "Next Friday" or "Last Tuesday", the plugin enforces static system prompt ambiguity rules:
+* `"next [weekday/unit]"`: Evaluated as the immediate next chronological occurrence after `Current Time`.
+* `"last [weekday/unit]"` / `"previous [weekday/unit]"`: Evaluated as the most recent past occurrence prior to `Current Time`.
+* `"this [weekday]"`: Evaluated as the occurrence within the current calendar week containing `Current Time`.
+
+### Confidence Thresholds & Metadata (`.ai`)
+
+When `minConfidence` is supplied in options (e.g. `parseAI("...", { minConfidence: 0.8 })`), any LLM response returning a confidence score below that threshold produces a `Tempo` instance with `isValid === false`.
+
+Every resolved `Tempo` instance returned by `parseAI` (and other AI functions) has a non-writable, frozen `.ai` metadata descriptor containing execution audit data:
+```typescript
+const dt = await parseAI("Christmas 2026", { debug: true });
+console.log(dt.ai);
+// {
+// provider: 'openai',
+// cached: false,
+// confidence: 0.95,
+// ambiguous: false,
+// granularity: 'day',
+// rawIso: '2026-12-25T00:00:00',
+// rawPrompt: 'Christmas 2026', // Present when debug is enabled
+// normalizedPrompt: 'christmas 2026' // Present when debug is enabled
+// }
+```
+
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/ai.index.md
+
+
+
+# @magmacomputing/tempo-plugin-ai
+
+
+
+
+
+> [!WARNING]
+> **🧪 EXPERIMENTAL PLUGIN**
+> This plugin relies on Generative AI. While it uses strict JSON schemas and validation to force deterministic outputs, LLMs (especially smaller models) can still hallucinate complex calendar math. We are actively collecting feedback on prompt engineering and model reliability. Please report any strange behavior or unexpected hallucinations on the [Magma GitHub Bug Report Form](https://github.com/magmacomputing/magma/issues/new?template=bug_report_ai.yml)!
+>
+> [!CAUTION]
+> **LLM Output Disclaimer**: Magma Computing Solutions and the Tempo core maintainers provide `@magmacomputing/tempo-plugin-ai` "as-is" without warranty of any kind. Large Language Models are probabilistic text generators, not deterministic calculators. Developers and organization operators are solely responsible for validating AI-generated date and time outputs before relying on them in financial, legal, medical, or time-critical production systems.
+
+Tempo community plugin for LLM-powered natural language parsing.
+
+This plugin bridges the gap between deterministic date-math and unstructured NLP inputs, utilizing large language models (like Gemini, Groq, or OpenAI) to safely and asynchronously parse, format, and process complex natural language temporal expressions into `Tempo` instances.
+
+> **Note**: This plugin is **not** a silver-bullet replacement for all your parsing needs! `Tempo.parse()` natively handles structured dates and formats phenomenally well using its Aliases, Layouts, and Snippets. The Tempo AI plugin is specifically designed to be an alternative path for handling completely unstructured, conversational human language that would otherwise be impossible to Regex.
+>
+> **CRITICAL SECURITY WARNING**: Raw LLM API keys must **never** be exposed in a client-side browser bundle or stored in browser storage (`localStorage`, `sessionStorage`, `IndexedDB`, or browser cache). Client-side storage is vulnerable to XSS attacks, malicious scripts, and browser extension extraction, which can result in API key theft and quota abuse. BYOK (Bring Your Own Key) is only secure on backend servers (Node, edge workers). For public frontend applications, you must route requests through a secure backend proxy service.
+
+## Ideal Use-Cases
+
+Good AI function candidates (such as `parseAI`) represent unstructured, conversational, or event-driven natural language expressions that are impossible to Regex or parse with standard relative offset rules:
+
+- **Holiday & Relative Calendar Math**: `"The Friday after Thanksgiving"`, `"The penultimate Tuesday before Christmas"`
+- **Named Cultural / Event Terms**: `"Star Wars Day at 5pm"`, `"A fortnight after Labor Day"`
+- **Conversational Relative Terms**: `"The last working day of Q3"`, `"Midday on the summer solstice"`
+
+> **Avoid Simple Offsets**: Phrases like `"in 5 minutes"`, `"tomorrow"`, or `"next Friday"` are natively intercepted and resolved by core `Tempo` without calling the LLM (unless `force: true` is passed).
+
+## Installation
+
+```bash
+npm install @magmacomputing/tempo-plugin-ai
+```
+
+## Setup & Usage
+
+```typescript
+import { parseAI, initAI, clearAiCache } from '@magmacomputing/tempo-plugin-ai';
+
+// Initialize with your BYOK API Key
+initAI({
+ providers: [
+ { id: 'openai', key: process.env.OPENAI_API_KEY, model: 'your-preferred-model' },
+ ],
+ debug: true // (Development-only) Enable verbose console logging
+});
+```
+
+> **Tip**: `initAI` is fully re-callable! You can invoke it multiple times during your application's lifecycle to hot-swap API keys or update your fallback providers mid-stream without restarting your server.
+
+```typescript
+// Parse a complex natural language string!
+const dt1 = await parseAI("The penultimate Tuesday before Thanksgiving in 2026");
+
+// Evict bad parses from the cache
+clearAiCache("The penultimate Tuesday before Thanksgiving in 2026");
+```
+
+## Execution Modes & Multi-Provider Options
+
+The AI plugin supports multi-provider execution strategies (`fallback`, `race`, `consensus`) and confidence filtering on per-request options:
+
+```typescript
+// 1. Fallback mode (default): query providers sequentially in array order until one succeeds
+const fallback = await parseAI("First Monday of November", {
+ mode: 'fallback', // Default strategy if omitted
+ minConfidence: 0.8 // Require at least 0.8 confidence threshold
+});
+
+// 2. Race mode: send concurrent requests to all providers, returning the fastest valid response
+const fastest = await parseAI("Third Friday of October", { mode: 'race' });
+
+// 3. Consensus mode: query providers concurrently and boost confidence when outputs agree
+const agreed = await parseAI("The penultimate Tuesday before Thanksgiving", {
+ mode: 'consensus',
+ minConfidence: 0.85 // Require at least 0.85 confidence threshold
+});
+```
+
+## Timeout Controls & SLAs
+
+Prevent hanging requests using the 3-tier timeout hierarchy (`call-site` > `provider` > `global` > `default 15s`):
+
+```typescript
+// Global timeout across all AI requests
+initAI({
+ providers: [
+ { id: 'groq', key: process.env.GROQ_API_KEY, options: { timeout: 2000 } } // 2s timeout for fast provider
+ ],
+ timeout: 5000 // 5s global default timeout
+});
+
+// Hard 3-second SLA override for a specific call-site
+const dt = await parseAI("Next Tuesday at 4pm", { timeout: 3000 });
+```
+
+## Debugging & Forced Evaluation
+
+When building your LLM queries, it is often useful to see exactly how AI functions route your data.
+
+**Global Debugging**
+Passing `debug: true` into `initAI` is intended for **development environments only**. It will globally log system prompts, localized context, and raw LLM responses to the console. Because prompts, context, and responses may contain user-supplied or sensitive data, disable `debug: true` or redact sensitive logs in production.
+
+**Forced Evaluation**
+If a relative phrase (like `"Next Friday"`) would normally be resolved by the native `Tempo` engine or read from existing cache, you can skip native pre-parsing and cache lookups by passing `force: true`. The resulting LLM response is still written to `Tempo.cache` for subsequent lookups:
+
+```typescript
+const dt = await parseAI("Next Friday", {
+ anchor: '2026-09-01T00:00:00Z',
+ force: true, // Skips native pre-parsing & cache lookup; forces an LLM request (result is cached)
+ debug: true // Overrides the global debug flag for this specific request
+});
+```
+
+## Documentation Topics
+
+> [!IMPORTANT]
+> **Production Recommendation**: Due to the complexities of LLM APIs, including caching gotchas, context injection, rate limits, and calendar math hallucinations, we politely but firmly recommend reading the three dedicated guides below before deploying this plugin in a production environment.
+
+To learn more about configuring and optimizing the AI Plugin, check out the dedicated guides:
+- [Provider Architecture & Security](./ai.architecture.md) (BYOK vs Proxy patterns, Frontend Security)
+- [Context & Natural Language Parsing](./ai.context.md) (How Timezone and Locale are injected)
+- [Rate Limits & Cache Management](./ai.rate-limits.md) (Tracking API quotas, handling 429 errors, and custom Redis caches)
+
+## Licensing
+
+This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required.
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/ai.rate-limits.md
+
+# Rate Limits & Cache Management
+
+When using third-party AI APIs, your application is subject to strict rate limits.
+
+The plugin automatically tracks these limits by reading the standard `x-ratelimit-*` HTTP headers returned by providers like OpenAI and Groq.
+
+## Tracking Quota Real-time
+
+Quota and rate-limit metadata can be inspected in two convenient ways:
+
+### 1. Request-Locked Instance Metadata (`dt.ai.limits`)
+Every `Tempo` instance returned by `parseAI` includes a frozen `.ai.limits` snapshot representing the exact rate limit state returned by the provider HTTP headers for *that specific request*:
+
+```typescript
+const dt = await parseAI("The third Friday of next month");
+
+if (dt.ai?.limits) {
+ console.log(`Remaining Tokens: ${dt.ai.limits.remainingTokens}`);
+ console.log(`Remaining Requests: ${dt.ai.limits.remainingRequests}`);
+ console.log(`Resets At: ${dt.ai.limits.resetAt?.format('{hh}:{mi}:{ss}')}`);
+}
+```
+
+### 2. Global State Utility (`getAiRateLimits()`)
+For quick status checks or global monitoring across the application lifecycle, `getAiRateLimits()` exposes the stats from the most recent LLM request:
+
+```typescript
+import { getAiRateLimits } from '@magmacomputing/tempo-plugin-ai';
+
+// Returns global stats from the most recent LLM proxy request
+const stats = getAiRateLimits();
+
+if (stats) {
+ console.log(`Remaining Tokens: ${stats.remainingTokens}`);
+ console.log(`Remaining Requests: ${stats.remainingRequests}`);
+ console.log(`Limits Reset At: ${stats.resetAt?.format('{hh}:{mi}:{ss}')}`);
+}
+```
+
+## Handling Quota Exhaustion (429s)
+
+If you actually exhaust your quota and the provider rejects the request (e.g., HTTP 429 Too Many Requests), the plugin will instantly attempt to failover to the next provider in your configuration array.
+
+If all providers fail, the plugin will throw a `TempoAiError`. This custom error class includes a highly valuable `retryAt` property:
+
+```typescript
+import { parseAI, TempoAiError } from '@magmacomputing/tempo-plugin-ai';
+
+try {
+ const dt = await parseAI("The third Friday of next month");
+} catch (error) {
+ if (error instanceof TempoAiError && error.code === 429) {
+ // Safely queue the remaining batch of dates until your minute-limit resets!
+ console.warn(`All API quotas exhausted. Retry after: ${error.retryAt}`);
+ }
+}
+```
+
+## Cache Management
+
+By default, Tempo AI functions integrate directly with `Tempo.cache` (`BoundedCache`) to store pre-resolved ISO 8601 results, drastically reducing LLM API calls and network latency on repetitive queries.
+
+### Array Processing & Token Economics
+
+When you pass an array of inputs to AI functions (such as `parseAI`), the plugin intentionally does **not** batch them into a single massive LLM request. Instead, it iterates through the array and processes each item individually.
+
+This is by design for three critical reasons:
+1. **Cache Efficiency**: Individual processing allows AI functions to instantly resolve duplicate strings against `Tempo.cache`, saving massive amounts of API tokens. If you pass an array of 10,000 dates, but only 1,000 are unique, only 1,000 network requests are made.
+2. **Token Economics**: A single request consumes ~100 tokens (System Prompt + User String + Output ISO). Given that frontier models cost cents per million tokens, the risk of array-misalignment bugs (see below) far outweighs the negligible savings of batching system prompts.
+3. **Deterministic Safety**: LLMs are language models, not arrays. If you pass 50 strings, smaller models often hallucinate and return 49 strings, completely breaking your array indexing. By querying sequentially, we guarantee a strict 1:1 mapping and ensure one invalid string doesn't crash the entire batch.
+
+> [!WARNING]
+> **Granular Time Gotcha**: The cache key is automatically salted with the **calendar date** (`yyyy-mm-dd`) of the execution anchor. By default this uses the system execution date, but when `options.anchor` is explicitly set, it uses the caller-provided anchor date. Note that keeping a fixed anchor date retains the same cache key across midnight boundaries, so an automatic midnight cache miss is not guaranteed.
+
+### Soft Errors in Array Batches
+
+When processing arrays of inputs, an unparseable input or provider failure on one item will throw an error by default, stopping execution. Passing `softErrors: true` allows AI functions to return invalid `Tempo` instances (`isValid === false`) for failing items while completing the rest of the array:
+
+```typescript
+const dates = await parseAI(["Thanksgiving 2026", "INVALID_PROMPT_STRING"], { softErrors: true });
+console.log(dates[0].isValid); // true
+console.log(dates[1].isValid); // false
+```
+
+### Static Glossary Seeding
+
+In addition to dynamic cache lookups, `initAI` can be initialized with a pre-seeded `BoundedCache` or synchronous `Map` containing immortal static business terms (e.g. company glossaries). Static entries bypass TTL expiration and LLM network requests:
+
+```typescript
+const glossary = new Map([
+ ['fiscal_q3_start', '2026-07-01T00:00:00Z'],
+ ['annual_shutdown', '2026-12-24T00:00:00Z']
+]);
+
+initAI({
+ providers: [{ id: 'openai', key: process.env.OPENAI_API_KEY }],
+ cache: glossary
+});
+
+const start = await parseAI('fiscal_q3_start'); // Resolves instantly from static cache without hitting network!
+```
+
+### Bypassing Cache & Forcing Network Requests
+Passing `cache: false` disables reading and writing to the cache, but native pre-parsing may still resolve standard phrases. To guarantee an LLM provider request while disabling caching of the response, combine `force: true` with `cache: false`:
+
+```typescript
+// Forces an LLM network request and prevents reading or writing to cache
+const dt = await parseAI("The last Friday before Christmas", { force: true, cache: false });
+```
+
+### Evicting Bad Parses
+If the LLM hallucinates or returns an incorrect absolute date, you can explicitly purge the string from the cache:
+
+```typescript
+import { clearAiCache } from '@magmacomputing/tempo-plugin-ai';
+
+// Evict a single string
+clearAiCache("2nd tuesday in nov");
+```
+
+### Forcing a Refresh
+If you want to explicitly query the LLM again and *overwrite* the existing cache entry with the new result, use the `force: true` flag:
+
+```typescript
+const dt = await parseAI("Q3_START", { force: true });
+```
+
+### Extensible Caching (Enterprise)
+For edge environments or custom application architectures, you can provide custom cache instances via `initAI({ cache })` or `Tempo.init({ cache })`!
+
+You can provide any object that implements the standard **synchronous** `Map` interface (`get`, `set`, `has`, `delete`). Note that all cache adapter methods must execute synchronously, as the cache lookup engine does not await promise-returning cache operations.
+
+```typescript
+// Custom synchronous cache implementation
+initAI({
+ providers: [{ id: 'groq', key: '...' }],
+ cache: new MyCustomSyncCache()
+});
+```
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/astro.index.md
+
+
+
+# @magmacomputing/tempo-plugin-astro
+
+
+
+
+
+This is a Community plugin for the [Tempo](https://github.com/magmacomputing/magma) library that calculates the exact astronomical season (Equinoxes and Solstices) for any date using the **Jean Meeus polynomial algorithm**.
+
+> [!NOTE]
+> **Mean-Polynomial Approximation (Ch. 27)**
+> This plugin specifically implements the mean-polynomial calculation from Chapter 27 of Meeus' *Astronomical Algorithms*. To keep the library extremely lightweight, it omits the massive periodic correction tables required for exact apparent calculations. It is strictly enforced to support the mathematical range of **-1000 to +3000 AD**.
+
+Because it is a true astronomical calculation rather than a fixed calendar date, it precisely determines the exact minute the sun crosses the celestial equator. It is also **hemisphere-aware**: by configuring your Tempo instance with a `sphere` (e.g., `sphere: 'south'`), the plugin accurately flips the Vernal Equinox from Spring to Autumn.
+
+::: info Meteorological vs Astronomical
+Unlike Tempo's built-in **Meteorological** `season` Term — which rigidly snaps to the 1st day of calendar months — this **Astronomical** plugin calculates the dynamic, true solar boundaries that shift slightly year-over-year.
+:::
+## Installation
+
+```bash
+npm install @magmacomputing/tempo-plugin-astro
+```
+
+## Usage
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+import { AstroTerm } from '@magmacomputing/tempo-plugin-astro';
+
+// Pass the plugin to `Tempo.init` to register it into the runtime.
+Tempo.init({
+ plugins: [AstroTerm]
+});
+
+const t = new Tempo('2026-03-20');
+
+// Get the Astronomical Event mapping
+console.log(t.term.astro);
+// Output: 'Vernal'
+```
+
+### Response Payload
+
+When resolving the term, the plugin intelligently returns the correct astronomical event and its corresponding traditional season based on your configured hemisphere (`sphere`):
+
+```javascript
+{
+ key: 'Vernal', // Flips to 'Autumnal' if sphere is set to 'south'
+ season: 'Spring', // Flips to 'Autumn' if sphere is set to 'south'
+ sphere: 'north', // Flips to 'south' if sphere is set to 'south'
+ event: 'Equinox',
+ group: 'astronomy',
+ year: 2026,
+ month: 3,
+ day: 20,
+ hour: 14,
+ minute: 45,
+ second: 0
+}
+```
+
+You can also access the full metadata object containing the sub-second precision fields via the `astronomy` term:
+
+```typescript
+console.log(t.term.astronomy);
+// Output: { key: 'Vernal', group: 'astronomy', year: 2026, month: 3, day: 20, hour: 14, minute: 45, ... }
+```
+
+::: tip Did you know?
+**Seasons:** `t.term.astronomy.season` returns the *Astronomical* season calculated by the precise timing of solstices and equinoxes. This will often differ from `t.term.season.key` in the core library, which uses standard Meteorological/Civil calendar boundaries (e.g., 1st of the month).
+:::
+
+## Licensing
+
+This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required.
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/batch.index.md
+
+
+
+# @magmacomputing/tempo-plugin-batch
+
+
+
+
+
+This is a Community plugin for the [Tempo](https://github.com/magmacomputing/magma) library that parallelizes massive epoch mutation tasks across worker threads utilizing lock-free `SharedArrayBuffer` architecture for extreme throughput.
+
+::: tip Perfect For
+Heavy data ETL pipelines, massive IoT telemetry ingestion, financial ledger chronometrics, and any parallel bulk date-processing workloads.
+:::
+
+## Installation
+
+```bash
+npm install @magmacomputing/tempo-plugin-batch
+```
+
+## Usage
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+import { BatchPlugin } from '@magmacomputing/tempo-plugin-batch';
+
+Tempo.init({
+ plugins: [BatchPlugin]
+});
+
+// Assume `epochs` is a massive array of integers representing timestamps
+const epochs = [1700000000000, 1700000001000, /* ... millions more ... */];
+
+// Mutate millions of dates concurrently using the worker pool
+// The engine automatically splits the payload and offloads to workers!
+const batchResult = await Tempo.batch(epochs, { weeks: 1 });
+
+console.log(batchResult); // Returns an array of mutated timestamp integers
+```
+
+### Rehydration
+
+By default, `Tempo.batch` returns an array of primitive `number` timestamps to maximize throughput over the thread boundary. If you need fully-fledged `Tempo` objects back, pass `{ rehydrate: true }`:
+
+```typescript
+// Returns an array of Tempo instances instead of integers
+const tempoInstances = await Tempo.batch(epochs, { weeks: 1 }, { rehydrate: true });
+```
+
+### Graceful Degradation
+
+If the host environment does not support `SharedArrayBuffer` (or if it is blocked by CORS/COOP headers in the browser), the orchestrator intelligently and transparently falls back to using traditional `postMessage` structural cloning chunks to ensure execution never halts.
+
+## Licensing
+
+This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required.
+
+For commercial licensing options, please contact Magma Computing.
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/finance.index.md
+
+
+
+# @magmacomputing/tempo-plugin-finance
+
+
+
+
+
+A specialized namespace plugin for Tempo that provides fiscal year and financial date utilities.
+
+## Installation
+
+```bash
+npm install @magmacomputing/tempo-plugin-finance
+```
+
+## Usage
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+import { FinanceNamespace } from '@magmacomputing/tempo-plugin-finance';
+
+// Register the namespace
+Tempo.extend(FinanceNamespace);
+
+const t = new Tempo('2024-07-01');
+
+// Evaluate static properties
+console.log(t.finance.fiscalQuarter); // 3
+console.log(t.finance.taxYear); // 2024
+
+// Evaluate functional closures
+console.log(t.finance.isFiscalYearStart()); // false
+```
+
+## Licensing
+
+This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required.
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/snap.index.md
+
+
+
+# @magmacomputing/tempo-plugin-snap
+
+
+
+
+
+A Community plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides robust time rounding and snapping functionality (e.g., snapping to the nearest 15 minutes or 1 hour block) for calendar and scheduling applications.
+
+By default, the plugin effortlessly snaps dates to a configurable minute-interval. This is particularly useful when building UI components like time-pickers, ensuring data boundaries align perfectly with application logic.
+
+### 💡 User Notes: Why Sub-Second Snapping?
+While `hours` and `minutes` cover most UI use cases, sub-second precision (`ms`, `us`, `ns`) is invaluable for:
+1. **Telemetry & Log Aggregation**: Snapping high-frequency jittery timestamps to the nearest `100ms` or `500ms` bucket for cleaner charts and analysis.
+2. **Video & Audio Synchronization**: Multimedia frame rates require precise timing. Snap to the nearest `16ms` (approx 60fps) or `40ms` (25fps) to align data points with visual boundaries.
+3. **Database & API Normalization**: Truncating or snapping Tempo's native nanosecond precision to the nearest `ms` before sending payloads ensures your local application state perfectly matches remote databases that don't support microseconds.
+4. **Performance Benchmarking**: Grouping execution times into buckets (e.g., nearest `10ms`) for histograms.
+
+## Installation
+
+```bash
+npm install @magmacomputing/tempo-plugin-snap
+```
+
+## Usage
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+import { SnapPlugin } from '@magmacomputing/tempo-plugin-snap';
+
+// Pass the plugin to `Tempo.init` to register it into the runtime.
+Tempo.init({
+ plugins: [SnapPlugin]
+});
+
+const t = new Tempo('2026-06-01T14:08:00Z');
+
+// Snaps to the nearest 15 minutes by default
+const snapped = t.snap();
+console.log(snapped.format('{hh}:{mi}')); // "14:15"
+
+// Or explicitly provide units and intervals
+const snapHour = t.snap({ hh: 1 });
+const snapSecond = t.snap({ ss: 30 });
+const snapMs = t.snap({ ms: 100 });
+
+// Force snapping direction instead of standard rounding
+const snapUp = t.snap({ mi: 15, direction: 'up' });
+const snapDown = t.snap({ mi: 15, direction: 'down' });
+```
+
+## Licensing
+
+This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required.
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/sync.index.md
+
+
+
+# @magmacomputing/tempo-plugin-sync
+
+
+
+
+
+This is a Community plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides lock-free, nanosecond-accurate cross-thread time synchronization using `SharedArrayBuffer` and `Atomics`.
+
+::: tip Perfect For
+High-frequency trading platforms, real-time multiplayer game servers, distributed microservice tracing, and extreme-precision scientific telemetry.
+:::
+
+## Installation
+
+```bash
+npm install @magmacomputing/tempo-plugin-sync
+```
+
+## Usage
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+import { SyncPlugin } from '@magmacomputing/tempo-plugin-sync';
+
+Tempo.init({
+ plugins: [SyncPlugin]
+});
+
+// Master Thread: Start the clock
+const clock = Tempo.sync.startClock({ updateIntervalMs: 1 });
+const buffer = clock.buffer; // Pass this SharedArrayBuffer to your workers
+```
+
+### Reading from Worker Threads
+
+To read the synchronized time from inside a worker thread, pass the `SharedArrayBuffer` via `workerData` and instantiate an `AtomicReader`.
+
+```typescript
+// worker.ts
+import { workerData } from 'node:worker_threads';
+import { AtomicReader } from '@magmacomputing/tempo-plugin-sync';
+
+// Hydrate the reader using the master buffer
+const reader = new AtomicReader(workerData.buffer);
+
+// 1. Get raw milliseconds (O(1) Atomic Read)
+const ms = reader.now();
+
+// 2. Get high-precision BigInt nanoseconds
+const ns = reader.nowNano();
+
+// 3. Hydrate a brand new Tempo instance with exact precision
+const t = reader.getTempo();
+```
+
+## Licensing
+
+This is a **Community** plugin. It is completely free and open-source for personal and commercial use. No license token is required.
+
+For commercial licensing options, please contact Magma Computing.
+
+
+
+
+---
+
+
+
+# Document: 9-plugins/ticker.index.md
+
+
+
+# @magmacomputing/tempo-plugin-ticker
+
+
+
+
+
+This is a premium plugin for the [Tempo](https://github.com/magmacomputing/magma) library that provides a high-performance continuous execution loop (Ticker) based on temporal mathematics.
+
+For details on how to unlock and use these features, see our [License Key Guide](./_setup.index.md).
+
+::: info High Performance Loop
+Unlike standard `setInterval` or `requestAnimationFrame`, the Ticker plugin leverages Tempo's robust temporal core to ensure exact sub-millisecond precision, making it ideal for games, complex UI animations, and accurate state synchronization.
+:::
+
+## Installation
+
+```bash
+npm install @magmacomputing/tempo-plugin-ticker
+```
+
+## Usage
+
+To use the Ticker, pass the plugin to `Tempo.init` to ensure it registers securely alongside your license.
+
+```typescript
+import { Tempo } from '@magmacomputing/tempo';
+import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker';
+
+// A valid premium license token is required to use this plugin
+Tempo.init({
+ license: 'YOUR_LICENSE_TOKEN',
+ plugins: [TickerPlugin]
+});
+
+// You can access Ticker-based execution loops through the Tempo API:
+const ticker = Tempo.ticker({ seconds: 1 });
+```
+
+### Direct Access
+If you need to access the [Reporting & Registry](#reporting-registry) API (like `Ticker.active`), you should import the `Ticker` namespace:
+
+```typescript
+import { Ticker } from '@magmacomputing/tempo-plugin-ticker';
+
+console.log(Ticker.active);
+```
+
+## 🚀 Key Features
+
+The Ticker supports a unified **Options** object, enabling professional resource management and semantic duration-based intervals.
+
+### 1. Semantic Intervals (Duration Objects)
+Instead of raw numeric seconds, you can use `DurationLike` objects or shorthand keys for clarity. This is especially powerful for variable-length intervals like **months**.
+
+```typescript
+// Pulse exactly once a month
+await using monthly = Tempo.ticker({ months: 1 });
+
+// You can also use highly compact shorthand keys
+await using concise = Tempo.ticker({ hh: 1, mi: 30 }); // every 1h 30m
+
+// Pulse every time a new #quarter begins
+await using quarterly = Tempo.ticker({ '#quarter': 1 });
+```
+
+### 2. Term-Based Intervals
+Ticker intervals can be driven by any registered **Term**. This is powerful for syncing with business cycles or daily shifts.
+
+> **Snapping vs Shifting:** Use directional shorthands (like `>`) to snap pulses exactly to the **boundaries** of the term (e.g., the very start of the morning). Using numeric values (like `1`) performs a relative shift, which preserves your current time-offset into the next period (e.g. two hours into a time-period will always be two hours into the next time-period).
+
+```typescript
+// Snap and pulse exactly at the start of every 'morning', 'afternoon', etc.
+using shiftTicker = Tempo.ticker({ '#timeOfDay': '>' }, (t) => {
+ console.log(`New period started: ${t.term.tod}`);
+});
+```
+
+### 3. Stop Conditions (Resource Management)
+Prevent memory leaks and runaway processes by setting a built-in termination condition.
+
+```typescript
+// Pattern A: Stop after exactly 5 ticks (defaults to 1-second interval)
+using tickerA = Tempo.ticker({ limit: 5 }, (t) => console.log(t));
+
+// Pattern B: Stop when a specific virtual time is reached (Inclusive)
+using tickerB = Tempo.ticker({
+ seconds: 10, // Plural DurationLike property
+ until: '2024-12-25T12:00:00'
+}, (t) => console.log(t));
+
+// Pattern C: Stop immediately (Limit: 0 is strictly honored)
+using tickerC = Tempo.ticker({ limit: 0 });
+```
+
+### 4. Virtual Clock (Seeding)
+To create a **Virtual Clock** that increments from a specific point rather than using the system time, use the `seed` option:
+
+```typescript
+// Starts at '2024-01-01', then increments by 1 day per pulse
+await using daily = Tempo.ticker({
+ days: 1,
+ seed: '2024-01-01'
+});
+```
+
+### 5. Backwards Tickers (Countdowns)
+By providing a **negative** interval, you can create a Ticker that moves backwards in time.
+
+```typescript
+// Count down from 10 seconds, moving backwards 1s at a time
+using countdown = Tempo.ticker({ seconds: -1, seed: "00:00:10" }, (t, stop) => {
+ console.log(t.format('{ss}'));
+ if (t.ss === 0) stop();
+});
+```
+
+## Usage Patterns
+
+### 1. Resource Management (Recommended)
+
+Using the `using` and `await using` keywords ensures that Tickers are automatically stopped when they go out of scope.
+
+```typescript
+// Pattern A: Automatic cleanup for callback-based ticker
+{
+ using ticker = Tempo.ticker((t) => render(t)); // Defaults to a 1-second pulse
+} // interval stops automatically here
+
+// Pattern B: Automatic cleanup for async generator
+{
+ await using ticker = Tempo.ticker(1);
+ for await (const t of ticker) {
+ if (done) break;
+ }
+} // generator is closed and interval stops here
+```
+
+### 2. Manual Control (Programmatic Stop)
+
+If you are not using the `using` or `await using` keywords, or if you need to stop the Ticker from outside its own loop (e.g., in a separate event handler), you can manually call the `stop()` method on the Ticker object.
+
+```typescript
+// Pattern A: Stop a callback-based ticker
+const tickerA = Tempo.ticker(1, (t) => console.log(t));
+// ... later
+tickerA.stop();
+
+// Pattern B: Stop an async generator externally
+const tickerB = Tempo.ticker(1);
+
+(async () => {
+ for await (const t of tickerB) {
+ console.log(t.toString());
+ }
+ console.log('Ticker has been gracefully stopped.');
+})();
+
+// Close the generator from somewhere else
+setTimeout(() => {
+ tickerB.stop();
+}, 5000);
+```
+### 3. Event Listeners (.on)
+Instead of (or in addition to) the constructor callback, you can register listeners for the `'pulse'`, `'stop'`, and `'catch'` events.
+All listeners use the same callback signature: `(t, stop) => {}`.
+
+```typescript
+const ticker = Tempo.ticker(1);
+ticker.on('pulse', (t) => console.log('Listener A:', t.fmt.weekTime));
+ticker.on('pulse', (t) => console.log('Listener B:', t.fmt.weekTime));
+ticker.on('stop', (t) => console.log('Ticker stopped at:', t.fmt.weekTime));
+```
+For `'stop'` listeners, the `stop` callback argument is included for signature consistency; however, invoking it after stop has already occurred is a no-op.
+
+### 4. Manual Pulsing (.pulse)
+In some scenarios, you may want to drive a Ticker manually (e.g., from a UI event or a WebSocket message) while still benefiting from the Ticker's internal state management and listeners.
+
+```typescript
+const ticker = Tempo.ticker({ seconds: 1 }); // Still has a 1s duration logic
+// ...
+ticker.pulse(); // Manually advance and notify listeners
+```
+
+## 🧟 Zombie Tickers (Warning) {#zombie-tickers-warning}
+
+In a Node.js environment, `Tempo.ticker()` uses background timers (`setTimeout`) to drive its pulses. If you do not explicitly stop a Ticker, it becomes a **"Zombie Ticker"** that continues to run indefinitely, even if the variable that created it has gone out of scope.
+
+### The Risks:
+- **Process Hangs**: Node.js will not exit a process if there are active timers. Undisposed Tickers are a common cause of "mysterious hangs" at the end of test runs.
+- **Test Inconsistency**: Leaked Tickers can continue to fire while subsequent tests are running, leading to flaky assertions and "impossible" state changes.
+- **Memory Leaks**: Each active Ticker maintains closures that prevent garbage collection of the `Tempo` instance and its listeners.
+
+### The Solution:
+Always use the **Disposer Pattern** (`using` or `await using`) or a `try...finally` block to guarantee cleanup:
+
+```typescript
+// ✅✅ BEST: Automatic cleanup via 'using'
+{
+ using ticker = Tempo.ticker(1);
+ // ... logic ...
+} // Stays clean: ticker stopped automatically here
+
+// ✅ GOOD: Manual cleanup in finally block (Required for captured variables)
+let ticker;
+try {
+ ticker = Tempo.ticker(1, (t) => { ... });
+ // ... assertions ...
+} finally {
+ ticker?.stop(); // Prevents "Zombie Tickers" even if assertions fail
+}
+```
+
+::: warning
+If you are using `const` or `let` without a `finally` block, an assertion failure will skip the `stop()` call, leaving a live timer in the event loop. Always prefer the `using` keyword or `try...finally` for industrial-grade resource management.
+:::
+
+### `Ticker` Object
+The object returned by `Tempo.ticker()` (or an instance of the `Ticker` class) implements the following interface:
+
+| Method / Property | Description |
+| :--- | :--- |
+| `on(event, cb)` | Registers a listener for the `'pulse'`, `'stop'`, or `'catch'` events. |
+| `pulse()` | Manually triggers a pulse, advances state, and notifies listeners. Returns the new `Tempo`. |
+| `info` | Read-only getter returning `{ next, ticks, limit, interval, stopped }`. |
+| `stop()` | Stops the Ticker, clears active timers, and immediately resolves any pending async iteration Promises. |
+| `[Symbol.dispose]` | Standard cleanup for `using` blocks. |
+| `[Symbol.asyncDispose]` | Standard async cleanup for `await using` blocks. |
+| `[Symbol.asyncIterator]` | Standard async iteration support (for `for await` loops). |
+
+## Reporting & Registry {#reporting-registry}
+
+The `Ticker` class maintains a static registry of all currently active Tickers. This is useful for debugging, monitoring, or cleanup checks.
+
+### `Ticker.active`
+A static getter that returns an array of [`Ticker.Snapshot`](#tickersnapshot) objects for all active (non-stopped) Tickers.
+
+```typescript
+import { Ticker } from '@magmacomputing/tempo-plugin-ticker';
+
+// Get a report of all running tickers
+const reports = Ticker.active;
+
+reports.forEach(({ ticker, next, ticks }) => {
+ console.log(`Ticker ${ticker} next pulse: ${next}, ticks so far: ${ticks}`);
+});
+```
+
+#### `Ticker.Snapshot`
+```typescript
+type Snapshot = {
+ ticker: Instance; // The Ticker instance (Proxy) itself
+ next: Tempo; // The next Tempo value to be emitted
+ ticks: number; // Number of pulses emitted so far
+ limit?: number; // The configured limit (if any)
+ interval: object; // The duration-based interval
+ stopped: boolean; // Whether the ticker is stopped
+}
+```
+
+## 🎯 One-Shot Ticker (Meeting Alerts)
+
+You can use the Ticker as a "one-shot" timer for specific events by simply specifying a **seed** value. This is perfect for setting up a single alert (e.g., for a meeting) that cleans itself up immediately after firing.
+
+::: tip
+**Seed-Only Logic**: Providing a `seed` (as a string or in an options object) without any other duration-based keys (`seconds`, `minutes`, etc.) or a `limit` implies a `limit: 1`.
+
+Effectively, `Tempo.ticker('Fri 10am')` and `Tempo.ticker({ seed: 'Fri 10am' })` and `Tempo.ticker({ seed: 'Fri 10am', limit: 1 })` are all treated as one-shot Tickers.
+
+**Inclusive Boundaries**: Termination conditions (`limit` and `until`) are **inclusive**. A Ticker with `limit: 1` will pulse exactly once before stopping.
+:::
+
+```typescript
+// Pattern A: Implicit one-shot via string seed
+Tempo.ticker('Friday 10am', (t) => {
+ console.log(`Meeting alert: ${t.format('{hh}:{mi}')}`);
+});
+
+// Pattern B: Explicit one-shot via options
+const event = { meeting: 'Friday 10am' };
+
+Tempo.ticker({
+ seed: { value: 'meeting', event }
+}, (t) => {
+ console.log(`Meeting alert: ${t.format('{hh}:{mi}')}`);
+});
+```
+
+::: warning
+**Future Seeds**: If the `seed` is in the future, the Ticker will remain dormant (waiting) until that time is reached. **Most Tickers emit an initial pulse immediately** (at the `seed` time or "now"), but a future seed will delay that first pulse until the specified time.
+:::
+
+::: danger
+**Persistence**: Ticker timers exist only **in-memory**. If the driving process (e.g., Node.js) terminates, any scheduled future pulses (including those from future seeds) are lost. For critical long-term scheduling, consider an external persistent job runner.
+:::
+
+::: warning
+While `limit: 1` handles the stop condition automatically, always remember that if you are using long-running Tickers without a limit, you **must** use the [Disposer Pattern](#zombie-tickers-warning) or manual `stop()` to avoid memory leaks and zombie processes.
+:::
+
+## 🧭 Advanced: Syncing Multiple Clocks
+
+If you need to show multiple timezones on a dashboard, avoid creating multiple Tickers. Instead, use a single **Master Ticker** to drive all views. This prevents "drift" between the clocks and is much more efficient.
+
+### Using Signals (Recommended)
+
+Signals (from Preact, Solid, or Vue) are perfect for this "one source, many views" pattern.
+
+```typescript
+// 1. Master source of truth
+const now = signal(new Tempo());
+
+// 2. Drive the master from a single ticker
+using _ = Tempo.ticker(1, (t) => now.value = t);
+
+// 3. Derived timezones update automatically and stay 100% in sync
+const sydney = computed(() => now.value.set({ timeZone: 'Australia/Sydney' }));
+const london = computed(() => now.value.set({ timeZone: 'Europe/London' }));
+```
+
+### Using Async Generators (Framework-Agnostic)
+
+If you are not using a reactive framework, you can use the same pattern with an `AsyncGenerator` to derive all clocks from a single pulse.
+
+```typescript
+// One generator, one interval, zero drift.
+await using master = Tempo.ticker(1);
+
+for await (const t of master) {
+ const clocks = {
+ sydney: t.set({ timeZone: 'Australia/Sydney' }),
+ ny: t.set({ timeZone: 'America/New_York' }),
+ london: t.set({ timeZone: 'Europe/London' })
+ };
+
+ renderDashboard(clocks);
+}
+```
+
+## Licensing
+
+This is a **Premium** plugin. Usage requires an active, cryptographically signed Tempo license token with the `ticker` scope enabled.
+
+::: tip
+