@athanlab/mcp 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/files.js ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Everything this server writes lands inside ATHANLAB_OUTPUT_DIR.
3
+ *
4
+ * A filename from an agent is treated as untrusted: only its last path
5
+ * segment is kept, characters that are unsafe on any common filesystem are
6
+ * dropped, `..` and leading dots cannot survive, the extension is forced to
7
+ * the audio format, and the resolved path is checked to still be inside the
8
+ * output directory. Writes go to a temporary file in the same directory and
9
+ * are renamed into place, so an interrupted download never leaves a
10
+ * half-written file under the final name (and a rename replaces a symlink
11
+ * rather than writing through it).
12
+ */
13
+ import { randomBytes } from 'node:crypto';
14
+ import { createWriteStream } from 'node:fs';
15
+ import { mkdir, rename, rm, stat, writeFile } from 'node:fs/promises';
16
+ import path from 'node:path';
17
+ import { Readable } from 'node:stream';
18
+ import { pipeline } from 'node:stream/promises';
19
+ import { AthanLabError } from './api/errors.js';
20
+ export const AUDIO_EXTENSIONS = ['wav', 'mp3'];
21
+ /** Extensions stripped before the forced one is added ("take.mp3" asked as wav → "take.wav"). */
22
+ const STRIPPED_EXTENSIONS = /\.(wav|wave|mp3|mpeg|mpga|m4a|aac|ogg|oga|opus|flac|webm|aiff?|txt|json)$/i;
23
+ const WINDOWS_RESERVED = /^(con|prn|aux|nul|com\d|lpt\d)$/i;
24
+ const MAX_NAME_BYTES = 180;
25
+ /** Keep letters (any script, Burmese included), marks, digits and . _ - ; collapse the rest to '-'. */
26
+ function cleanSegment(input) {
27
+ return input
28
+ .normalize('NFC')
29
+ .replace(/[\p{Cc}\p{Cf}]/gu, '')
30
+ .replace(/[^\p{L}\p{M}\p{N}._-]+/gu, '-')
31
+ .replace(/-{2,}/g, '-')
32
+ .replace(/^[.\-_]+/, '')
33
+ .replace(/[.\-_ ]+$/, '');
34
+ }
35
+ function truncateBytes(input, maxBytes) {
36
+ if (Buffer.byteLength(input) <= maxBytes)
37
+ return input;
38
+ let out = '';
39
+ for (const ch of input) {
40
+ if (Buffer.byteLength(out + ch) > maxBytes)
41
+ break;
42
+ out += ch;
43
+ }
44
+ return out;
45
+ }
46
+ /**
47
+ * A safe file name with the given extension. `fallback` (already safe) is
48
+ * used when nothing usable is left of `requested`.
49
+ */
50
+ export function sanitizeFilename(requested, ext, fallback) {
51
+ // Last segment only, whichever separator the agent used.
52
+ const base = (requested ?? '').split(/[\\/]/).pop() ?? '';
53
+ let stem = cleanSegment(base.replace(STRIPPED_EXTENSIONS, ''));
54
+ stem = stem.replace(STRIPPED_EXTENSIONS, '');
55
+ if (!stem || /^\.+$/.test(stem))
56
+ stem = fallback;
57
+ if (WINDOWS_RESERVED.test(stem))
58
+ stem = `_${stem}`;
59
+ stem = truncateBytes(stem, MAX_NAME_BYTES).replace(/[.\-_ ]+$/, '') || fallback;
60
+ return `${stem}.${ext}`;
61
+ }
62
+ /** A safe single directory name (for a script's out_subdir). */
63
+ export function sanitizeDirname(requested, fallback) {
64
+ const base = requested.split(/[\\/]/).filter(Boolean).pop() ?? '';
65
+ let name = cleanSegment(base);
66
+ if (!name || /^\.+$/.test(name))
67
+ name = fallback;
68
+ if (WINDOWS_RESERVED.test(name))
69
+ name = `_${name}`;
70
+ return truncateBytes(name, MAX_NAME_BYTES) || fallback;
71
+ }
72
+ /** `root/name`, refusing anything that would resolve outside `root`. */
73
+ export function resolveInside(root, ...segments) {
74
+ const resolvedRoot = path.resolve(root);
75
+ const target = path.resolve(resolvedRoot, ...segments);
76
+ const rel = path.relative(resolvedRoot, target);
77
+ if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
78
+ throw AthanLabError.client('file_error', 'Refusing to write outside ATHANLAB_OUTPUT_DIR.');
79
+ }
80
+ return target;
81
+ }
82
+ export async function ensureDir(dir) {
83
+ try {
84
+ await mkdir(dir, { recursive: true });
85
+ }
86
+ catch (err) {
87
+ throw AthanLabError.client('file_error', `Could not create the output directory: ${err.message}`);
88
+ }
89
+ }
90
+ export async function exists(file) {
91
+ try {
92
+ await stat(file);
93
+ return true;
94
+ }
95
+ catch {
96
+ return false;
97
+ }
98
+ }
99
+ /** `dir/name`, or `dir/stem-<suffix>.ext` (then -2, -3 …) when that file already exists. Never overwrites. */
100
+ export async function uniquePath(dir, name, suffix) {
101
+ const first = resolveInside(dir, name);
102
+ if (!(await exists(first)))
103
+ return first;
104
+ const ext = path.extname(name);
105
+ const stem = name.slice(0, name.length - ext.length);
106
+ for (let n = 1; n < 1000; n++) {
107
+ const candidate = resolveInside(dir, `${stem}-${suffix}${n > 1 ? `-${n}` : ''}${ext}`);
108
+ if (!(await exists(candidate)))
109
+ return candidate;
110
+ }
111
+ throw AthanLabError.client('file_error', 'Could not find a free file name in the output directory.');
112
+ }
113
+ function tempPathFor(target) {
114
+ return path.join(path.dirname(target), `.${path.basename(target)}.${randomBytes(4).toString('hex')}.part`);
115
+ }
116
+ export async function writeFileAtomic(target, data) {
117
+ const tmp = tempPathFor(target);
118
+ try {
119
+ await writeFile(tmp, data);
120
+ await rename(tmp, target);
121
+ }
122
+ catch (err) {
123
+ await rm(tmp, { force: true }).catch(() => { });
124
+ throw AthanLabError.client('file_error', `Could not write ${path.basename(target)}: ${err.message}`);
125
+ }
126
+ }
127
+ /**
128
+ * Stream a fetch Response body into `target` (atomically). Returns the bytes
129
+ * written; refuses a body larger than `maxBytes` or shorter than its
130
+ * Content-Length (a truncated download is not audio).
131
+ */
132
+ export async function writeResponseToFile(res, target, maxBytes) {
133
+ if (!res.body)
134
+ throw AthanLabError.client('file_error', 'The download had no body.');
135
+ const declared = Number.parseInt(res.headers.get('content-length') ?? '', 10);
136
+ if (Number.isFinite(declared) && declared > maxBytes) {
137
+ throw AthanLabError.client('file_error', `The download is larger than the ${Math.round(maxBytes / 1048576)} MB limit.`);
138
+ }
139
+ const tmp = tempPathFor(target);
140
+ let written = 0;
141
+ try {
142
+ const source = Readable.fromWeb(res.body);
143
+ source.on('data', (chunk) => {
144
+ written += chunk.length;
145
+ if (written > maxBytes)
146
+ source.destroy(new Error(`download exceeds ${maxBytes} bytes`));
147
+ });
148
+ await pipeline(source, createWriteStream(tmp));
149
+ if (Number.isFinite(declared) && written !== declared) {
150
+ throw new Error(`download was cut short (${written} of ${declared} bytes)`);
151
+ }
152
+ await rename(tmp, target);
153
+ return written;
154
+ }
155
+ catch (err) {
156
+ await rm(tmp, { force: true }).catch(() => { });
157
+ if (err instanceof AthanLabError)
158
+ throw err;
159
+ throw new AthanLabError({
160
+ code: 'download_failed',
161
+ message: `Could not save the audio: ${err.message}`,
162
+ source: 'network',
163
+ retryable: true,
164
+ });
165
+ }
166
+ }
167
+ //# sourceMappingURL=files.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files.js","sourceRoot":"","sources":["../src/files.ts"],"names":[],"mappings":"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"}
package/dist/guide.js ADDED
@@ -0,0 +1,96 @@
1
+ export const GUIDE_NAME = 'athanlab-writing-guide';
2
+ export const GUIDE_URI = 'athanlab://guides/writing';
3
+ export const GUIDE_TITLE = 'Writing text for AthanLab speech';
4
+ /** The one input every mode is shown reading (writing.ts NUMBER_EXAMPLE_INPUT). */
5
+ export const NUMBER_EXAMPLE_INPUT = 'ကျပ် ၁၂၃၄';
6
+ /** writing.ts NUMBER_MODE_EXAMPLES, verbatim. */
7
+ export const NUMBER_MODE_EXAMPLES = [
8
+ {
9
+ mode: 'smart',
10
+ reads: 'The default. Burmese digits as Burmese words — digit by digit after a leading zero and for runs of 9 or more digits, with ဒဿမ for a decimal point. Latin digits (1234) are left as written.',
11
+ spoken: 'ကျပ် တစ်ထောင့်နှစ်ရာ့သုံးဆယ့်လေး',
12
+ },
13
+ {
14
+ mode: 'place',
15
+ reads: 'Every number, in Burmese or Latin digits, as Burmese place words (up to 8 digits; longer runs digit by digit).',
16
+ spoken: 'ကျပ် တစ်ထောင့်နှစ်ရာ့သုံးဆယ့်လေး',
17
+ },
18
+ {
19
+ mode: 'digits',
20
+ reads: 'Burmese, one digit at a time: phone numbers, codes, IDs.',
21
+ spoken: 'ကျပ် တစ် နှစ် သုံး လေး',
22
+ },
23
+ {
24
+ mode: 'english',
25
+ reads: 'English number words.',
26
+ spoken: 'ကျပ် one thousand two hundred thirty four',
27
+ },
28
+ {
29
+ mode: 'english_digits',
30
+ reads: 'English, one digit at a time.',
31
+ spoken: 'ကျပ် one two three four',
32
+ },
33
+ ];
34
+ /** writing.ts SMART_MODE_DETAILS, verbatim. */
35
+ export const SMART_MODE_DETAILS = [
36
+ { input: 'ဖုန်း ၀၉၁၂၃', spoken: 'ဖုန်း သုည ကိုး တစ် နှစ် သုံး' },
37
+ { input: '၃.၅ ကီလို', spoken: 'သုံး ဒဿမ ငါး ကီလို' },
38
+ { input: 'ကျပ် 1234', spoken: 'ကျပ် 1234' },
39
+ { input: '၁၂၃၄၅၆၇၈၉', spoken: 'တစ် နှစ် သုံး လေး ငါး ခြောက် ခုနစ် ရှစ် ကိုး' },
40
+ ];
41
+ /** writing.ts DISPATCH_TOO_LONG_MESSAGE, verbatim. */
42
+ export const DISPATCH_TOO_LONG_MESSAGE = 'Part of the text is too long to speak in one piece. Add sentence punctuation and retry.';
43
+ /** The documented limits the guide quotes (OpenAPI x-limits). */
44
+ export const TEXT_MAX = 5000;
45
+ export const EXPANDED_TEXT_MAX = 10000;
46
+ function fmt(n) {
47
+ return n.toLocaleString('en-US');
48
+ }
49
+ /** The guide as Markdown. Optional sections appear only for features the live API has. */
50
+ export function writingGuide(features) {
51
+ const modes = NUMBER_MODE_EXAMPLES
52
+ .map(e => `- \`${e.mode}\` — ${e.reads}\n → ${e.spoken}`)
53
+ .join('\n');
54
+ const smart = SMART_MODE_DETAILS.map(d => `- ${d.input} → ${d.spoken}`).join('\n');
55
+ const sections = [
56
+ `# ${GUIDE_TITLE}`,
57
+ 'AthanLab reads exactly the text you send, in Burmese (Myanmar). A few habits make it sound natural — and keep a job from being refused. Refusals happen before any charge.',
58
+ `## Short sentences, with their punctuation
59
+ - End every sentence with \`။\`, \`!\` or \`?\`, and keep sentences short.
60
+ - Text is spoken sentence by sentence. A sentence too long for one piece is cut at a \`၊\`, then at a space — mid-word only as a last resort — so a long stretch without punctuation breaks in odd places.
61
+ - A piece that still cannot be made short enough is refused with 422 \`text_too_long\`: "${DISPATCH_TOO_LONG_MESSAGE}"
62
+ - \`athanlab_quote\` returns \`dispatches\`: how many pieces your text becomes.`,
63
+ `## Burmese script
64
+ - Write every word in Burmese script. The voices mispronounce Latin-script words.
65
+ - Spell names and loanwords the way they sound, in Burmese — \`အသံလက်ဘ်\`, not \`AthanLab\` — or leave them out.`,
66
+ `## Marks the voice does not read
67
+ - Quotation marks, brackets and \`…\` are not spoken well. Remove them, or put a \`၊\` where you want a short pause.
68
+ - Text that is only emoji, punctuation or whitespace has nothing to read: it is refused with 422 \`unspeakable_text\`.`,
69
+ `## Numbers (\`number_mode\`)
70
+ How digits are read. Each mode reading \`${NUMBER_EXAMPLE_INPUT}\`:
71
+ ${modes}
72
+
73
+ More \`smart\` readings:
74
+ ${smart}
75
+
76
+ Writing numbers out as words never costs extra, but the written-out text must stay within ${fmt(EXPANDED_TEXT_MAX)} characters; beyond that the job is refused with \`text_too_long\`.`,
77
+ `## Scripts, dialogue and video
78
+ - Split long scripts into sentence-sized lines, one job per line, each with its speaker's \`voice_id\` — \`athanlab_speak_script\` does exactly this.
79
+ - Every line then has its own audio file to place on a timeline, retime or replace; fixing one line regenerates — and costs — only that line.
80
+ - Jobs running at once are limited per account, so lines are queued and submitted as earlier ones finish.`,
81
+ `## Length and cost
82
+ - One job takes 1–${fmt(TEXT_MAX)} characters and costs exactly its length, in UTF-16 code units: a Burmese or Latin character counts 1, an emoji counts 2.
83
+ - \`athanlab_quote\` is free: it validates the text and reports the exact count. A failed job is refunded in full.`,
84
+ ];
85
+ if (features?.replacements) {
86
+ sections.push(`## Pronunciation replacements
87
+ - \`replacements\` rewrites only what is spoken: \`[{"from": "AthanLab", "to": "အသံလက်ဘ်"}]\`. The billed text stays yours.
88
+ - Use it for brand names, acronyms and codes the voices would otherwise misread. Up to ${features.replacements.maxRules} rules per job.`);
89
+ }
90
+ if (features?.speed) {
91
+ sections.push(`## Speed
92
+ - \`speed\` from ${features.speed.min} to ${features.speed.max} (1 = normal) changes pace without changing pitch. It does not change the price.`);
93
+ }
94
+ return `${sections.join('\n\n')}\n`;
95
+ }
96
+ //# sourceMappingURL=guide.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"guide.js","sourceRoot":"","sources":["../src/guide.ts"],"names":[],"mappings":"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"}
package/dist/index.js ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * athanlab-mcp — the AthanLab Developer API as an MCP server over stdio.
4
+ *
5
+ * ATHANLAB_API_KEY=ak_live_… npx athanlab-mcp
6
+ *
7
+ * stdout carries the MCP protocol; every log line goes to stderr.
8
+ */
9
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
10
+ import { loadFeatures } from './api/features.js';
11
+ import { ConfigError, loadConfig } from './config.js';
12
+ import { createLogger, redact } from './log.js';
13
+ import { createAthanLabServer } from './server.js';
14
+ import { VERSION } from './version.js';
15
+ async function main() {
16
+ const argv = process.argv.slice(2);
17
+ if (argv.includes('--version') || argv.includes('-v')) {
18
+ process.stdout.write(`${VERSION}\n`);
19
+ return;
20
+ }
21
+ if (argv.includes('--help') || argv.includes('-h')) {
22
+ process.stdout.write([
23
+ `athanlab-mcp ${VERSION} — AthanLab text-to-speech for MCP clients (stdio).`,
24
+ '',
25
+ 'Environment:',
26
+ ' ATHANLAB_API_KEY required: ak_live_ + 32 hex (Developer API page)',
27
+ ' ATHANLAB_API_BASE default https://api.athanlab.com/api/v1',
28
+ ' ATHANLAB_OUTPUT_DIR default ./athanlab-audio',
29
+ ' ATHANLAB_MAX_CHARS_PER_CALL optional safety cap per speak/script call',
30
+ '',
31
+ ].join('\n'));
32
+ return;
33
+ }
34
+ let config;
35
+ try {
36
+ config = loadConfig();
37
+ }
38
+ catch (err) {
39
+ if (err instanceof ConfigError) {
40
+ process.stderr.write(`athanlab-mcp: ${err.message}\n`);
41
+ process.exit(1);
42
+ }
43
+ throw err;
44
+ }
45
+ const logger = createLogger({
46
+ secrets: config.apiKey.ok ? [config.apiKey.key] : [],
47
+ level: process.env.ATHANLAB_LOG_LEVEL === 'debug' ? 'debug' : 'info',
48
+ });
49
+ if (!config.apiKey.ok)
50
+ logger.warn(config.apiKey.message);
51
+ const features = await loadFeatures(config.apiBase, { logger });
52
+ const { server } = createAthanLabServer({ config, features, logger });
53
+ const transport = new StdioServerTransport();
54
+ await server.connect(transport);
55
+ logger.info('athanlab-mcp ready', { version: VERSION, api_base: config.apiBase, output_dir: config.outputDir });
56
+ const shutdown = () => {
57
+ void server.close().finally(() => process.exit(0));
58
+ };
59
+ process.on('SIGINT', shutdown);
60
+ process.on('SIGTERM', shutdown);
61
+ }
62
+ main().catch((err) => {
63
+ process.stderr.write(`athanlab-mcp: ${redact(err instanceof Error ? err.message : String(err))}\n`);
64
+ process.exit(1);
65
+ });
66
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/lock.js ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * One athanlab_speak_script run per script folder at a time.
3
+ *
4
+ * Two runs on one folder would each read the manifest, plan the same
5
+ * unstarted lines and create them under different Idempotency-Keys: every
6
+ * line charged twice. So a run holds the folder from reading the manifest
7
+ * to writing its last line, and a second run on the same folder is refused
8
+ * at once with `script_running` (not queued: the first run can take up to
9
+ * an hour, longer than a client waits for one tool call).
10
+ *
11
+ * - Within this process (parallel tool calls, an agent retrying while its
12
+ * first call still runs) a Set of held folders decides, synchronously.
13
+ * - Across processes (two MCP clients sharing ATHANLAB_OUTPUT_DIR) a lock
14
+ * file created with O_EXCL decides. It sits next to the folder, in the
15
+ * output directory, so a quote-only call does not create the folder.
16
+ * A lock left by a process that died is taken over: its pid is gone (same
17
+ * host), or it is older than any run can last.
18
+ */
19
+ import { randomBytes } from 'node:crypto';
20
+ import { open, readFile, rm, stat } from 'node:fs/promises';
21
+ import { hostname } from 'node:os';
22
+ import { AthanLabError } from './api/errors.js';
23
+ /** Longer than the longest run: timeout_seconds is at most 3600, plus downloads. */
24
+ const STALE_AFTER_MS = 2 * 60 * 60 * 1000;
25
+ /** A lock file still being written by its creator has no content yet. */
26
+ const UNREADABLE_GRACE_MS = 60 * 1000;
27
+ const RETRY_AFTER_SECONDS = 30;
28
+ const held = new Set();
29
+ function running(dir, since) {
30
+ return new AthanLabError({
31
+ code: 'script_running',
32
+ message: `Another athanlab_speak_script call is already generating the script in ${dir}${since ? ` (since ${since})` : ''}. Nothing was sent.`,
33
+ source: 'client',
34
+ retryable: true,
35
+ retryAfterSeconds: RETRY_AFTER_SECONDS,
36
+ details: { directory: dir, ...(since ? { started_at: since } : {}) },
37
+ });
38
+ }
39
+ function pidAlive(pid) {
40
+ try {
41
+ process.kill(pid, 0);
42
+ return true;
43
+ }
44
+ catch (err) {
45
+ // EPERM: it exists, it just is not ours to signal.
46
+ return err.code === 'EPERM';
47
+ }
48
+ }
49
+ function parseInfo(raw) {
50
+ try {
51
+ const value = JSON.parse(raw);
52
+ if (typeof value.pid === 'number' && typeof value.host === 'string' && typeof value.started_at === 'string' && typeof value.token === 'string') {
53
+ return value;
54
+ }
55
+ }
56
+ catch {
57
+ // unreadable
58
+ }
59
+ return null;
60
+ }
61
+ /** The lock's holder, or null when the lock is gone or stale (and may be taken over). */
62
+ async function liveHolder(lockFile) {
63
+ let raw;
64
+ try {
65
+ raw = await readFile(lockFile, 'utf8');
66
+ }
67
+ catch (err) {
68
+ if (err.code === 'ENOENT')
69
+ return null;
70
+ throw err;
71
+ }
72
+ const info = parseInfo(raw);
73
+ if (!info) {
74
+ try {
75
+ const age = Date.now() - (await stat(lockFile)).mtimeMs;
76
+ return age > UNREADABLE_GRACE_MS ? null : 'unknown';
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ if (info.host === hostname()) {
83
+ // Our own pid: this process holds nothing under that name (the Set said so), so it is a leftover.
84
+ if (info.pid === process.pid || !pidAlive(info.pid))
85
+ return null;
86
+ }
87
+ const started = Date.parse(info.started_at);
88
+ if (!Number.isFinite(started) || Date.now() - started > STALE_AFTER_MS)
89
+ return null;
90
+ return info;
91
+ }
92
+ async function createExclusive(lockFile, info) {
93
+ let handle;
94
+ try {
95
+ handle = await open(lockFile, 'wx');
96
+ }
97
+ catch (err) {
98
+ if (err.code === 'EEXIST')
99
+ return false;
100
+ throw AthanLabError.client('file_error', `Could not create the script lock file: ${err.message}`);
101
+ }
102
+ try {
103
+ await handle.writeFile(`${JSON.stringify(info)}\n`);
104
+ }
105
+ finally {
106
+ await handle.close();
107
+ }
108
+ return true;
109
+ }
110
+ /**
111
+ * Hold `dir` (identified by its resolved path) until release(), using
112
+ * `lockFile` against other processes. Throws `script_running` when it is held.
113
+ */
114
+ export async function acquireScriptLock(dir, lockFile) {
115
+ // Synchronous check-and-set: two calls in this process cannot both pass.
116
+ if (held.has(dir))
117
+ throw running(dir, null);
118
+ held.add(dir);
119
+ const info = { pid: process.pid, host: hostname(), started_at: new Date().toISOString(), token: randomBytes(8).toString('hex') };
120
+ try {
121
+ let acquired = await createExclusive(lockFile, info);
122
+ if (!acquired) {
123
+ const holder = await liveHolder(lockFile);
124
+ if (holder)
125
+ throw running(dir, holder === 'unknown' ? null : holder.started_at);
126
+ // Stale: take it over, once. Losing that race to another process means it is running.
127
+ await rm(lockFile, { force: true });
128
+ acquired = await createExclusive(lockFile, info);
129
+ if (!acquired)
130
+ throw running(dir, null);
131
+ }
132
+ }
133
+ catch (err) {
134
+ held.delete(dir);
135
+ throw err;
136
+ }
137
+ let released = false;
138
+ return {
139
+ async release() {
140
+ if (released)
141
+ return;
142
+ released = true;
143
+ try {
144
+ // Only our own lock: if it was taken over as stale, it is not ours to delete.
145
+ const current = parseInfo(await readFile(lockFile, 'utf8'));
146
+ if (current?.token === info.token)
147
+ await rm(lockFile, { force: true });
148
+ }
149
+ catch {
150
+ // already gone
151
+ }
152
+ finally {
153
+ held.delete(dir);
154
+ }
155
+ },
156
+ };
157
+ }
158
+ //# sourceMappingURL=lock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lock.js","sourceRoot":"","sources":["../src/lock.ts"],"names":[],"mappings":"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"}
package/dist/log.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Logging and secret redaction.
3
+ *
4
+ * stdout is the MCP channel (JSON-RPC over stdio), so every log line goes to
5
+ * stderr. Every line, and every text a tool returns, passes through redact():
6
+ * the key is only ever placed in the X-API-Key header, but a defence in depth
7
+ * costs nothing — anything shaped like an AthanLab key is masked wherever it
8
+ * turns up.
9
+ */
10
+ const KEY_LIKE = /ak_live_[0-9A-Za-z]+/g;
11
+ export function redact(text, secrets = []) {
12
+ let out = text;
13
+ for (const secret of secrets) {
14
+ if (secret)
15
+ out = out.split(secret).join('ak_live_***');
16
+ }
17
+ return out.replace(KEY_LIKE, 'ak_live_***');
18
+ }
19
+ const ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
20
+ /** One JSON line per event on stderr (or the given sink, for tests). */
21
+ export function createLogger(opts = {}) {
22
+ const min = ORDER[opts.level ?? 'info'];
23
+ const sink = opts.sink ?? ((line) => { process.stderr.write(`${line}\n`); });
24
+ const secrets = opts.secrets ?? [];
25
+ const emit = (level, message, fields) => {
26
+ if (ORDER[level] < min)
27
+ return;
28
+ let line;
29
+ try {
30
+ line = JSON.stringify({ ts: new Date().toISOString(), level, msg: message, ...fields });
31
+ }
32
+ catch {
33
+ line = JSON.stringify({ ts: new Date().toISOString(), level, msg: message });
34
+ }
35
+ sink(redact(line, secrets));
36
+ };
37
+ return {
38
+ debug: (m, f) => emit('debug', m, f),
39
+ info: (m, f) => emit('info', m, f),
40
+ warn: (m, f) => emit('warn', m, f),
41
+ error: (m, f) => emit('error', m, f),
42
+ };
43
+ }
44
+ export const silentLogger = {
45
+ debug: () => { },
46
+ info: () => { },
47
+ warn: () => { },
48
+ error: () => { },
49
+ };
50
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,QAAQ,GAAG,uBAAuB,CAAA;AAExC,MAAM,UAAU,MAAM,CAAC,IAAY,EAAE,UAA6B,EAAE;IAClE,IAAI,GAAG,GAAG,IAAI,CAAA;IACd,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,MAAM;YAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;IACzD,CAAC;IACD,OAAO,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,aAAa,CAAC,CAAA;AAC7C,CAAC;AAWD,MAAM,KAAK,GAA6B,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,CAAA;AAEpF,wEAAwE;AACxE,MAAM,UAAU,YAAY,CAAC,OAIzB,EAAE;IACJ,MAAM,GAAG,GAAG,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,CAAA;IACvC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,CAAC,IAAY,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,GAAG,IAAI,IAAI,CAAC,CAAA,CAAC,CAAC,CAAC,CAAA;IACnF,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,IAAI,EAAE,CAAA;IAClC,MAAM,IAAI,GAAG,CAAC,KAAe,EAAE,OAAe,EAAE,MAAgC,EAAE,EAAE;QAClF,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,GAAG;YAAE,OAAM;QAC9B,IAAI,IAAY,CAAA;QAChB,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG,MAAM,EAAE,CAAC,CAAA;QACzF,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,EAAE,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAA;QAC9E,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAA;IAC7B,CAAC,CAAA;IACD,OAAO;QACL,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;QACpC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAClC,IAAI,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC,CAAC;QAClC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC;KACrC,CAAA;AACH,CAAC;AAED,MAAM,CAAC,MAAM,YAAY,GAAW;IAClC,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;IACf,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,IAAI,EAAE,GAAG,EAAE,GAAE,CAAC;IACd,KAAK,EAAE,GAAG,EAAE,GAAE,CAAC;CAChB,CAAA"}