{"_id":"@annexops/sdk","_rev":"4-e4bfc9f86eb7467e8819343a8aa92229","name":"@annexops/sdk","dist-tags":{"latest":"0.3.0"},"versions":{"0.1.0":{"name":"@annexops/sdk","version":"0.1.0","keywords":["annexops","eu-ai-act","article-12","ai-governance","compliance","runtime-logging","audit-log"],"license":"MIT","_id":"@annexops/sdk@0.1.0","maintainers":[{"name":"annexops","email":"ankit@annexops.com"}],"dist":{"shasum":"43c29a510ce7ac98ae29e988e0d477c541607a66","tarball":"https://registry.npmjs.org/@annexops/sdk/-/sdk-0.1.0.tgz","fileCount":21,"integrity":"sha512-U3Bo0tASfDgss31Wq7RKpsWNEo9agySnw6xmSoWW1KSHtACzok/Cms93Y8roxolPTXDfSVJeth5OSbuAuScJww==","signatures":[{"sig":"MEQCIFwN/ruTNvF0Eg3spMpSZb55021nSFq7EC9JyUfuwl4qAiBWDNHcTdgmTBu2CQ10VJlr9Wgh8RVTvn6KvyiSI59TXw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":46339},"main":"./dist/index.js","type":"module","_from":"file:annexops-sdk-0.1.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"annexops","email":"ankit@annexops.com"},"_resolved":"/private/var/folders/yf/ssxssqkd3pn0jyntmlngcjk40000gn/T/e719b39171cf1afd29a0d612f74b2960/annexops-sdk-0.1.0.tgz","_integrity":"sha512-U3Bo0tASfDgss31Wq7RKpsWNEo9agySnw6xmSoWW1KSHtACzok/Cms93Y8roxolPTXDfSVJeth5OSbuAuScJww==","_npmVersion":"10.8.2","description":"AnnexOps Runtime Logger — hash-at-source SDK for EU AI Act Article 12 runtime logging. Batches SHA-256 hashes of your model's inputs/outputs to the AnnexOps ingest endpoint; raw content never leaves your process.","directories":{},"_nodeVersion":"20.20.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.1.2","typescript":"^5.8.3","@types/node":"^20.17.30","@annexops/types":"0.0.0"},"_npmOperationalInternal":{"tmp":"tmp/sdk_0.1.0_1783122001898_0.05992283155203659","host":"s3://npm-registry-packages-npm-production"}},"0.1.1":{"name":"@annexops/sdk","version":"0.1.1","keywords":["annexops","eu-ai-act","article-12","ai-governance","compliance","runtime-logging","audit-log"],"license":"MIT","_id":"@annexops/sdk@0.1.1","maintainers":[{"name":"annexops","email":"ankit@annexops.com"}],"dist":{"shasum":"07bf6fcad247a357027aea2f3bac229ea34cb7de","tarball":"https://registry.npmjs.org/@annexops/sdk/-/sdk-0.1.1.tgz","fileCount":21,"integrity":"sha512-eLWwAGdZNxETQ/PRKu9Z3z4eatYibtzXoixozGba75nfB3Nyvzvb9RkletjPeZjMST5yGi8rcmZ9meZ72vJtiQ==","signatures":[{"sig":"MEUCIA/e/Trlr05Tb7hTIOkfXWhtmgYx8Zd5Sqh4CrU21dz/AiEAp29sZ106zfxd9YkRcb8N0/GTvREILVcAWiBSg0RrjSY=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":46164},"main":"./dist/index.js","type":"module","_from":"file:annexops-sdk-0.1.1.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"annexops","email":"ankit@annexops.com"},"_resolved":"/private/var/folders/yf/ssxssqkd3pn0jyntmlngcjk40000gn/T/2373d0143f22fca8ee75ffad23d1a4c5/annexops-sdk-0.1.1.tgz","_integrity":"sha512-eLWwAGdZNxETQ/PRKu9Z3z4eatYibtzXoixozGba75nfB3Nyvzvb9RkletjPeZjMST5yGi8rcmZ9meZ72vJtiQ==","_npmVersion":"10.8.2","description":"AnnexOps Runtime Logger — hash-at-source SDK for EU AI Act Article 12 runtime logging. Batches SHA-256 hashes of your model's inputs/outputs to the AnnexOps ingest endpoint; raw content never leaves your process.","directories":{},"_nodeVersion":"20.20.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.1.2","typescript":"^5.8.3","@types/node":"^20.17.30","@annexops/types":"0.0.0"},"_npmOperationalInternal":{"tmp":"tmp/sdk_0.1.1_1783125525786_0.22343412335644763","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@annexops/sdk","version":"0.2.0","keywords":["annexops","eu-ai-act","article-12","ai-governance","compliance","runtime-logging","audit-log"],"license":"MIT","_id":"@annexops/sdk@0.2.0","maintainers":[{"name":"annexops","email":"ankit@annexops.com"}],"dist":{"shasum":"64013a6ac7960cad05d87e55b3b334669cec58d4","tarball":"https://registry.npmjs.org/@annexops/sdk/-/sdk-0.2.0.tgz","fileCount":25,"integrity":"sha512-vP3DNe5ncK3ffqfdvAvdYxeU0mpd6rI7Tfdz3SSWcdtby/jdhB0QYUxcLBWpCgHO/OIIPhi8T7FmXqXVeN0k1w==","signatures":[{"sig":"MEUCIQDiv2J575Td+6Gote0jOlA+ZwHEB3Xxzxxjl8ciGhYpuQIgPsUjPU4QGjtoA3FL8GBTM9gaoPTS1IH6lTj1YZ3vRsA=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":76292},"main":"./dist/index.js","type":"module","_from":"file:annexops-sdk-0.2.0.tgz","types":"./dist/index.d.ts","engines":{"node":">=20"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"scripts":{"test":"vitest run","build":"tsc -p tsconfig.build.json","typecheck":"tsc --noEmit"},"_npmUser":{"name":"annexops","email":"ankit@annexops.com"},"_resolved":"/private/var/folders/yf/ssxssqkd3pn0jyntmlngcjk40000gn/T/261b09c035f86a7f8f3992f5b02cf103/annexops-sdk-0.2.0.tgz","_integrity":"sha512-vP3DNe5ncK3ffqfdvAvdYxeU0mpd6rI7Tfdz3SSWcdtby/jdhB0QYUxcLBWpCgHO/OIIPhi8T7FmXqXVeN0k1w==","_npmVersion":"10.9.8","description":"AnnexOps Runtime Logger — hash-at-source SDK for EU AI Act Article 12 runtime logging. Batches SHA-256 hashes of your model's inputs/outputs to the AnnexOps ingest endpoint; raw content never leaves your process.","directories":{},"_nodeVersion":"22.23.1","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"vitest":"^3.1.2","typescript":"^5.8.3","@types/node":"^20.17.30","@annexops/types":"0.0.0"},"_npmOperationalInternal":{"tmp":"tmp/sdk_0.2.0_1786080960280_0.35646206134242076","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@annexops/sdk","version":"0.3.0","type":"module","license":"MIT","description":"AnnexOps SDK — zero-dependency, hash-at-source clients for EU AI Act & GDPR: runtime logging (Article 12), consent receipts (Article 7), and private-database schema discovery for data mapping. Raw content and row values never leave your process.","keywords":["annexops","eu-ai-act","gdpr","article-12","article-7","ai-governance","compliance","runtime-logging","consent","schema-discovery","data-mapping","audit-log"],"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js"}},"types":"./dist/index.d.ts","engines":{"node":">=20"},"publishConfig":{"access":"public"},"devDependencies":{"@types/node":"^20.17.30","typescript":"^5.8.3","vitest":"^3.1.2","@annexops/types":"0.0.0"},"scripts":{"typecheck":"tsc --noEmit","test":"vitest run","build":"tsc -p tsconfig.build.json"},"main":"./dist/index.js","_id":"@annexops/sdk@0.3.0","_integrity":"sha512-LxBmg+sPRUGSoZ7G8iPvfZ74d02MJqZhsipbXfR6BR303MY9YDCnRxpUdNWzLb9p3dnlb4NGC8hMOwJTTJVPOw==","_resolved":"/private/var/folders/yf/ssxssqkd3pn0jyntmlngcjk40000gn/T/5a5bf76c10710705082b1ae1c59fd417/annexops-sdk-0.3.0.tgz","_from":"file:annexops-sdk-0.3.0.tgz","_nodeVersion":"22.23.1","_npmVersion":"10.9.8","dist":{"integrity":"sha512-LxBmg+sPRUGSoZ7G8iPvfZ74d02MJqZhsipbXfR6BR303MY9YDCnRxpUdNWzLb9p3dnlb4NGC8hMOwJTTJVPOw==","shasum":"21fa8cc967dfb6deb7c33d8f64efbfced8596b38","tarball":"https://registry.npmjs.org/@annexops/sdk/-/sdk-0.3.0.tgz","fileCount":31,"unpackedSize":100071,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIEdBZBPqmAlKTA9R3WdIVElERhy5KrkcnK8ZZZ02VtdvAiEAl7IBlGICMBqRBApYXMUgMfMtevYM6X0CHr9Apdoue9k="}]},"_npmUser":{"name":"annexops","email":"ankit@annexops.com"},"directories":{},"maintainers":[{"name":"annexops","email":"ankit@annexops.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/sdk_0.3.0_1786773780045_0.4067247109149672"},"_hasShrinkwrap":false}},"time":{"created":"2026-07-03T23:40:01.768Z","modified":"2026-08-15T06:03:00.329Z","0.1.0":"2026-07-03T23:40:02.055Z","0.1.1":"2026-07-04T00:38:45.906Z","0.2.0":"2026-08-07T05:36:00.433Z","0.3.0":"2026-08-15T06:03:00.187Z"},"license":"MIT","keywords":["annexops","eu-ai-act","gdpr","article-12","article-7","ai-governance","compliance","runtime-logging","consent","schema-discovery","data-mapping","audit-log"],"description":"AnnexOps SDK — zero-dependency, hash-at-source clients for EU AI Act & GDPR: runtime logging (Article 12), consent receipts (Article 7), and private-database schema discovery for data mapping. Raw content and row values never leave your process.","maintainers":[{"name":"annexops","email":"ankit@annexops.com"}],"readme":"# @annexops/sdk\n\nThe client SDK for **AnnexOps** — log a tamper-evident, hash-only trace of every inference your AI system makes and every consent grant/withdrawal your users make, and map where personal data lives across your databases — all without the underlying content or any row value ever leaving your process.\n\n> Engineering documentation, not legal advice. AnnexOps does not certify\n> conformity. Keeping these records **supports** (but does not by itself\n> discharge) your EU AI Act **Article 12** record-keeping and GDPR\n> **Article 7** consent-recording obligations — whether they satisfy Article\n> 12/7 for your system is a determination for you and your counsel.\n\n- **Hashes only, schema only** — `AILogger` computes `SHA-256(input)`/`SHA-256(output)`; `ConsentLogger` computes `SHA-256(subjectId)`/`SHA-256(consentText)`; `SchemaScanner` reads only column **names and types** from `information_schema`. All at source; raw content and row values are never transmitted.\n- **Zero runtime dependencies** — `node:crypto` + the global `fetch`.\n- **`SchemaScanner`** — map where personal data lives in a **private** database AnnexOps can't reach directly: run it inside your own network to push schema (table/column names + declared types only, never a row value) into your data map.\n- **Batched, retrying, idempotent** — the loggers buffer and flush automatically; retries are safe by construction.\n\n> This is the TypeScript/Node client. The **Python** mirror (`annexops_sdk`) and **Go** mirror offer the same wire contracts; any other language can POST directly to the same endpoints.\n\n## Install\n\n```sh\nnpm install @annexops/sdk\n```\n\nRequires Node.js ≥ 20. ESM.\n\n## Quickstart — AILogger (runtime inference log, Article 12)\n\nWrap your inference call in a few lines:\n\n```ts\nimport { AILogger } from '@annexops/sdk';\n\nconst logger = new AILogger({\n  apiKey: process.env.ANNEXOPS_API_KEY!, // mint in AnnexOps → Settings → API keys\n  systemKey: 'my-model-v1',              // a name you choose per AI system\n});\n\n// per inference — pass the RAW text; the SDK hashes it at source and discards it\nlogger.logInference({ input: prompt, output: response, modelVersion: 'gpt-4o', confidence: 0.98 });\n\n// flush on graceful shutdown\nawait logger.close();\n```\n\nThat is the complete integration. `logInference()` is non-blocking; events buffer locally and flush automatically every 5 seconds (or when 100 accumulate). Always call `logger.close()` on shutdown to flush the remainder.\n\n### What arrives in AnnexOps\n\nEach event is POSTed to the AnnexOps ingest endpoint and appended to a per-`(org, system_key)` SHA-256 hash chain — the tamper-evident Article 12 record. In the AnnexOps portal (**Runtime Logs**), signed in, you map each `system_key` to a registered AI system, browse events, check chain integrity, and export the Article 12 record. Verification and export happen in the portal, never via the SDK or your API key.\n\n### Configuration\n\n| Option | Default | Description |\n|---|---|---|\n| `apiKey` | — (required) | Your `ak_live_…` key. Never logged or echoed in errors. |\n| `baseUrl` | `https://annex-ops.vercel.app/api/v1` | Override for a custom domain. |\n| `systemKey` | — | Client-level default; overridable per `logInference()` call. |\n| `flushAt` | `100` | Buffer size that triggers an automatic flush (hard-clamped to 500). |\n| `flushIntervalMs` | `5000` | Interval-flush cadence in ms. `0` disables the timer. |\n| `maxRetries` | `3` | Retry attempts after the initial POST (4 total). |\n| `backoffBaseMs` | `500` | Full-jitter backoff base (ms). |\n| `backoffCapMs` | `30000` | Full-jitter backoff cap (ms). |\n| `requestTimeoutMs` | `10000` | Per-request timeout (ms). |\n| `fetch` | `globalThis.fetch` | Injectable transport — useful for tests or proxies. |\n| `onRejected` | — | Callback for per-event server-side rejections. |\n| `onError` | — | Callback for transport/HTTP errors after retries are exhausted. |\n\n### Hashes only, never content\n\nThe SDK computes `input_hash` and `output_hash` at source using SHA-256. Raw prompt and response text **never leave your process** — only lowercase 64-char hex digests are sent. If you'd rather hash yourself:\n\n```ts\nimport { sha256Hex } from '@annexops/sdk';\n\nlogger.logInference({ inputHash: sha256Hex(prompt), outputHash: sha256Hex(response) });\n```\n\n## Quickstart — ConsentLogger (GDPR consent receipts, Article 7)\n\n`ConsentLogger` is the sibling client for a customer's own cookie banner / consent UI: it POSTs batched, subject-hashed grant/withdraw receipts, appended to a tamper-evident chain that is your org's GDPR Article 7 consent record.\n\n```ts\nimport { ConsentLogger } from '@annexops/sdk';\n\nconst consentLogger = new ConsentLogger({\n  apiKey: process.env.ANNEXOPS_API_KEY!, // the same ak_live_… key as AILogger\n  purposeKey: 'marketing-emails',        // a name you choose per consent purpose\n});\n\n// per consent event — pass the RAW subject id and notice text; the SDK\n// hashes both at source and discards them\nconsentLogger.logConsent({\n  subjectId: user.email,               // or any stable identifier you use\n  action: 'grant',\n  consentText: cookieBannerNoticeText, // the exact text the subject was shown\n  consentVersion: 'v3',\n});\n\n// a withdrawal doesn't need consentText — it isn't restating what was withdrawn from\nconsentLogger.logConsent({ subjectId: user.email, action: 'withdraw' });\n\n// flush on graceful shutdown\nawait consentLogger.close();\n```\n\n**Subject identity is hashed differently from consent text.** `subjectId` (an email, a user id, a device id — usually low-entropy) is hashed at source with SHA-256 and then, server-side, run through a second HMAC step with a secret pepper AnnexOps holds — so a stored value can't be reversed by hashing a list of candidate emails yourself. `consentText` (a paragraph of notice copy — high-entropy) is a bare SHA-256, the same treatment `AILogger` gives your prompts/responses. Your app never needs to know this distinction: `logConsent()` handles it.\n\nIf you'd rather hash the subject id yourself before it reaches your own logging code, pass `subjectHash` (and/or `consentTextHash`) instead of the raw value — same mutual-exclusivity rule as `AILogger`'s `input`/`inputHash`.\n\n### What arrives in AnnexOps\n\nEach receipt is appended to a per-`(org, purpose_key)` SHA-256 hash chain. In the AnnexOps portal (**Consent**), signed in, you map each `purpose_key` to a registered consent purpose, browse receipts, check chain integrity, and see the derived opt-in count/rate. Verification happens in the portal, never via the SDK or your API key.\n\n### Configuration\n\nSame shape as `AILogger`'s (`apiKey`, `baseUrl`, `flushAt`, `flushIntervalMs`, `maxRetries`, `backoffBaseMs`, `backoffCapMs`, `requestTimeoutMs`, `fetch`, `onRejected`, `onError`), with `purposeKey` in place of `systemKey` — see the table above; every default is identical.\n\n## Quickstart — SchemaScanner (data-map schema push)\n\n`SchemaScanner` reads the **column metadata** of one of your databases — schema, table, and column *names* plus their declared *types* — and pushes it to AnnexOps, where it is classified into a data map. **Only names and types are read; no row is ever queried, and no data value ever leaves your process.** The query is a fixed `information_schema` read the SDK owns — you never pass SQL.\n\nYou bring your own driver (`pg` or `mysql2`); the SDK bundles none. Give it a `runner` that executes exactly the SQL it hands you and returns the rows:\n\n```ts\nimport { SchemaScanner } from '@annexops/sdk';\nimport { Client } from 'pg'; // your own dependency\n\nconst db = new Client({ connectionString: process.env.DATABASE_URL });\nawait db.connect();\n\nconst scanner = new SchemaScanner({\n  apiKey: process.env.ANNEXOPS_API_KEY!, // the same ak_live_… key as the loggers\n  storeKey: 'prod-postgres',             // a stable name you choose per database\n  source: 'postgres',                    // 'postgres' | 'mysql'\n  storeName: 'Production Postgres',      // optional label\n});\n\n// the runner runs exactly the SQL passed to it — the SDK never interpolates\nconst result = await scanner.scanAndPush((sql) => db.query(sql).then((r) => r.rows));\n\nawait db.end();\nconsole.log(`Pushed ${result.elements_ingested} columns; ${result.classifications} classified.`);\n```\n\nFor **MySQL**, pass `source: 'mysql'` and a `mysql2` runner:\n\n```ts\nimport mysql from 'mysql2/promise'; // your own dependency\n\nconst conn = await mysql.createConnection(process.env.MYSQL_URL!);\nconst scanner = new SchemaScanner({ apiKey, storeKey: 'prod-mysql', source: 'mysql' });\nawait scanner.scanAndPush((sql) => conn.query(sql).then(([rows]) => rows as any[]));\nawait conn.end();\n```\n\n`scan(runner)` and `push()` are also available separately if you want to inspect the buffered schema between the two. A very large schema is truncated at 50 000 columns and reported as `completeness: 'partial'`.\n\n### What arrives in AnnexOps\n\nEach column becomes a schema element (name + type) classified into a data category, and feeds your Record of Processing Activities (Article 30) and DSAR fulfilment automatically. In the AnnexOps portal (**Data Stores**), signed in, you review the discovered stores, their columns, and the personal-data classification; special-category (Article 9) and criminal-offence (Article 10) columns are flagged for a human to confirm. The scan uses a **read-only** connection and reads `information_schema` only — grant the scanning credential the least privilege that lets it read the catalog and nothing more.\n\n> **Python:** the same `SchemaScanner` is mirrored in the stdlib-only [`annexops_sdk`](../sdk-python/README.md) package — `SchemaScanner(api_key=..., store_key=..., source=...)`, `scan_and_push(runner)`, with a `psycopg`/`pymysql` runner.\n\n### Configuration\n\n| Option | Default | Description |\n|---|---|---|\n| `apiKey` | — (required) | Your `ak_live_…` key. Never logged or echoed in errors. |\n| `storeKey` | — (required) | Stable identifier for this database — resolves-or-creates the store on first push. |\n| `source` | — (required) | `'postgres'` or `'mysql'` — selects the fixed introspection query. |\n| `storeName` | — | Optional human-readable label, set on first push. |\n| `baseUrl` | `https://annex-ops.vercel.app/api/v1` | Override for a custom domain. |\n| `maxRetries` | `3` | Retry attempts after the initial POST (4 total). |\n| `backoffBaseMs` | `500` | Full-jitter backoff base (ms). |\n| `backoffCapMs` | `30000` | Full-jitter backoff cap (ms). |\n| `requestTimeoutMs` | `10000` | Per-request timeout (ms). |\n| `fetch` | `globalThis.fetch` | Injectable transport — useful for tests or proxies. |\n\n## Retries & idempotency\n\nBoth loggers retry network errors, timeouts, `5xx`, and `429` with full-jitter exponential backoff (base 500 ms, cap 30 s, 3 retries). Neither retries a `401` or `400` — those are permanent for that batch. Every retry re-sends the **identical batch with the same `event_id`s** (generated once at log time), so duplicate delivery is safe: the server de-duplicates per stream (`(org, system_key, event_id)` for `AILogger`; `(org, purpose_key, event_id)` for `ConsentLogger`).\n\n## Flush & close\n\nThe buffer flushes when it reaches `flushAt`, every `flushIntervalMs` (the timer is `unref()`'d, so it never holds your process open), on an explicit `await logger.flush()`, and on `await logger.close()`.\n\n**Always `await logger.close()` on graceful shutdown** to avoid losing buffered events:\n\n```ts\nprocess.on('SIGTERM', async () => {\n  await logger.close();\n  await consentLogger.close();\n  process.exit(0);\n});\n```\n\n`flush()` and `close()` resolve with `{ accepted: number; rejected: IngestRejection[] }`. Permanently invalid events (field-level validation failures) appear in `rejected` and are never retried; valid events in the same batch are still accepted.\n\n## Key handling\n\nMint an API key in **AnnexOps → Settings → API keys**. The full key (`ak_live_…`) is shown **exactly once** — copy it then; it cannot be retrieved again. The same key works for both `AILogger` and `ConsentLogger` — there is nothing extra to mint.\n\n- Store it in an environment variable (e.g. `ANNEXOPS_API_KEY`); pass it as the `apiKey` option.\n- **Never commit it** — the `ak_live_` prefix is a recognizable secret pattern that scanners flag.\n- **Rotate** by minting a new key, updating your environment, then revoking the old one (revoked keys return `401` immediately).\n\n## Documentation\n\nFull integration guide — including a language-agnostic HTTP contract for services not on JS/TS: **[annex-ops.vercel.app/docs/annexops-runtime-logger-guide.pdf](https://annex-ops.vercel.app/docs/annexops-runtime-logger-guide.pdf)**\n\n## License\n\nMIT © AnnexOps\n","readmeFilename":"README.md"}