layoutbridge-spm 0.0.1 → 1.0.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.
Files changed (79) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/LICENSE +26 -0
  3. package/README.de.md +318 -0
  4. package/README.md +304 -4
  5. package/THIRD-PARTY-NOTICES.md +2878 -0
  6. package/dist/src/mcp/config.js +125 -0
  7. package/dist/src/mcp/dispatch.js +123 -0
  8. package/dist/src/mcp/license/free-tools.js +16 -0
  9. package/dist/src/mcp/license/gate.js +209 -0
  10. package/dist/src/mcp/license/index.js +96 -0
  11. package/dist/src/mcp/license/instance-name.js +56 -0
  12. package/dist/src/mcp/license/key-source.js +58 -0
  13. package/dist/src/mcp/license/lemonsqueezy-verifier.js +207 -0
  14. package/dist/src/mcp/license/placeholder-verifier.js +10 -0
  15. package/dist/src/mcp/license/polar-ids.js +63 -0
  16. package/dist/src/mcp/license/polar-verifier.js +265 -0
  17. package/dist/src/mcp/license/product-ids.js +72 -0
  18. package/dist/src/mcp/license/state-store.js +91 -0
  19. package/dist/src/mcp/license/types.js +11 -0
  20. package/dist/src/mcp/logging.js +270 -0
  21. package/dist/src/mcp/product-info.js +48 -0
  22. package/dist/src/mcp/server.js +250 -0
  23. package/dist/src/mcp/setup.js +608 -0
  24. package/dist/src/mcp/start.cjs +95 -0
  25. package/dist/src/mcp/tool-definition.js +2 -0
  26. package/dist/src/mcp/tools.js +2004 -0
  27. package/dist/src/mcp/typography-rules.js +70 -0
  28. package/dist/src/transport/com.js +250 -0
  29. package/dist/src/transport/osascript.js +171 -0
  30. package/dist/src/transport/queue.js +19 -0
  31. package/dist/src/transport/script-bundle.js +92 -0
  32. package/dist/src/transport/transport.js +30 -0
  33. package/indesign/commands/10-app.js +6 -0
  34. package/indesign/commands/20-documents.js +513 -0
  35. package/indesign/commands/22-document-setup.js +118 -0
  36. package/indesign/commands/23-save-package.js +255 -0
  37. package/indesign/commands/25-export.js +379 -0
  38. package/indesign/commands/26-export-image.js +197 -0
  39. package/indesign/commands/27-textdefaults.js +151 -0
  40. package/indesign/commands/28-selection.js +174 -0
  41. package/indesign/commands/30-pages.js +1032 -0
  42. package/indesign/commands/31-view.js +210 -0
  43. package/indesign/commands/32-sections.js +203 -0
  44. package/indesign/commands/35-parents.js +428 -0
  45. package/indesign/commands/37-layout.js +760 -0
  46. package/indesign/commands/40-frames.js +836 -0
  47. package/indesign/commands/41-shapes.js +505 -0
  48. package/indesign/commands/42-groups.js +117 -0
  49. package/indesign/commands/43-layers.js +291 -0
  50. package/indesign/commands/44-effects.js +274 -0
  51. package/indesign/commands/45-frame-options.js +247 -0
  52. package/indesign/commands/47-anchored.js +287 -0
  53. package/indesign/commands/50-modify.js +781 -0
  54. package/indesign/commands/52-align.js +139 -0
  55. package/indesign/commands/55-fit.js +106 -0
  56. package/indesign/commands/60-images.js +771 -0
  57. package/indesign/commands/62-embed.js +120 -0
  58. package/indesign/commands/69-placeholders.js +127 -0
  59. package/indesign/commands/70-text.js +567 -0
  60. package/indesign/commands/71-outlines.js +92 -0
  61. package/indesign/commands/72-place-text.js +366 -0
  62. package/indesign/commands/74-stories.js +116 -0
  63. package/indesign/commands/75-tables.js +676 -0
  64. package/indesign/commands/76-text-variables.js +260 -0
  65. package/indesign/commands/77-story-options.js +86 -0
  66. package/indesign/commands/78-frame-lines.js +304 -0
  67. package/indesign/commands/79-check-typography.js +303 -0
  68. package/indesign/commands/80-styles.js +1620 -0
  69. package/indesign/commands/81-composition.js +355 -0
  70. package/indesign/commands/82-object-styles.js +263 -0
  71. package/indesign/commands/83-indents-tabs.js +237 -0
  72. package/indesign/commands/84-import-styles.js +142 -0
  73. package/indesign/commands/85-swatches.js +112 -0
  74. package/indesign/commands/86-gradients.js +185 -0
  75. package/indesign/commands/87-replace-swatch.js +180 -0
  76. package/indesign/commands/88-replace-font.js +217 -0
  77. package/indesign/interpreter.js +758 -0
  78. package/package.json +55 -6
  79. package/rules/typography-rules.json +31 -0
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Speicher fuer das letzte Pruefergebnis (E-60): eine kleine JSON-Datei
3
+ * neben der Serverkonfiguration. Fehlt sie oder ist sie unlesbar, gilt
4
+ * "kein Ergebnis" – nie ein Absturz.
5
+ */
6
+ import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
7
+ import { dirname } from 'node:path';
8
+ const STATUSES = ['valid', 'invalid', 'expired', 'product_mismatch', 'unreachable'];
9
+ function optionalString(value) {
10
+ return typeof value === 'string' ? value : null;
11
+ }
12
+ /** Text der Speicherdatei pruefen; alles Unbrauchbare ergibt null. */
13
+ export function parseLicenseState(text) {
14
+ let raw;
15
+ try {
16
+ raw = JSON.parse(text);
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
22
+ return null;
23
+ const record = raw;
24
+ const fp = record['key_fingerprint'];
25
+ const status = record['status'];
26
+ const checkedAt = record['checked_at'];
27
+ if (typeof fp !== 'string' || fp.length === 0)
28
+ return null;
29
+ if (typeof status !== 'string' || !STATUSES.includes(status))
30
+ return null;
31
+ if (typeof checkedAt !== 'string' || Number.isNaN(Date.parse(checkedAt)))
32
+ return null;
33
+ return {
34
+ key_fingerprint: fp,
35
+ status: status,
36
+ checked_at: checkedAt,
37
+ last_valid_at: optionalString(record['last_valid_at']),
38
+ expires_at: optionalString(record['expires_at']),
39
+ instance_id: optionalString(record['instance_id']),
40
+ /*
41
+ * A-8 (13.09.2026, E-64): das fruehere Feld customer trug die
42
+ * E-Mail-Adresse des Kunden. Es wird nicht mehr gelesen und beim
43
+ * naechsten Schreiben ueberschrieben – so verschwindet es von selbst.
44
+ */
45
+ verifier: typeof record['verifier'] === 'string' ? record['verifier'] : '',
46
+ message: typeof record['message'] === 'string' ? record['message'] : '',
47
+ };
48
+ }
49
+ export class FileLicenseStateStore {
50
+ location;
51
+ constructor(location) {
52
+ this.location = location;
53
+ }
54
+ load() {
55
+ let text;
56
+ try {
57
+ text = readFileSync(this.location, 'utf8');
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ return parseLicenseState(text);
63
+ }
64
+ save(state) {
65
+ mkdirSync(dirname(this.location), { recursive: true });
66
+ writeFileSync(this.location, JSON.stringify(state, null, 2) + '\n', { encoding: 'utf8', mode: 0o600 });
67
+ }
68
+ clear() {
69
+ try {
70
+ unlinkSync(this.location);
71
+ }
72
+ catch {
73
+ /* schon weg */
74
+ }
75
+ }
76
+ }
77
+ /** Speicher im Arbeitsspeicher – fuer Tests. */
78
+ export class MemoryLicenseStateStore {
79
+ location = 'memory';
80
+ state = null;
81
+ load() {
82
+ return this.state === null ? null : { ...this.state };
83
+ }
84
+ save(state) {
85
+ this.state = { ...state };
86
+ }
87
+ clear() {
88
+ this.state = null;
89
+ }
90
+ }
91
+ //# sourceMappingURL=state-store.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Lizenzschranke (Auftrag A-6 mit Nachtrag 05.09.2026, E-60): Typen.
3
+ *
4
+ * Diese Schicht kennt weder InDesign noch den Transport. Sie kennt nur den
5
+ * Schluessel, ein gespeichertes Pruefergebnis und eine austauschbare
6
+ * Prueffunktion (LicenseVerifier). Der eigentliche Aufruf beim
7
+ * Zahlungsdienst kommt als eigene Klasse an diese Schnittstelle, sobald der
8
+ * Dienst mit Endpunkten beauftragt ist.
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Protokoll: eine Zeile je Aufruf (Spezifikation, allgemeine Anforderung 11).
3
+ * Jede Instanz schreibt in eine eigene Datei, damit sich mehrere Server nicht
4
+ * gegenseitig ins Protokoll schreiben.
5
+ *
6
+ * Wichtig: nie auf die Standardausgabe schreiben – dort laeuft das
7
+ * MCP-Protokoll. Nur in die Datei und notfalls auf die Standardfehlerausgabe.
8
+ *
9
+ * Datensparsamkeit (Auftrag A-8 des Vermarktungsprojekts, 13.09.2026,
10
+ * Vermerk E-64): Der Aufrufsatz `args` wird nicht mehr roh geschrieben.
11
+ * Festgehalten werden die Parameternamen; Werte nur dort, wo sie fuer die
12
+ * Fehlersuche noetig und harmlos sind – Zahlen, Wahrheitswerte, Kennungen,
13
+ * Indizes, Aufzaehlungswerte. Zeichenketten, die Dokumentinhalt sein
14
+ * koennen, werden durch eine Laengenangabe ersetzt. Pfade behalten ihre
15
+ * Form, das Benutzerverzeichnis wird durch "~" ersetzt. Wie tief
16
+ * protokolliert wird, steuert LAYOUTBRIDGE_SPM_LOG_LEVEL.
17
+ */
18
+ import { appendFileSync, mkdirSync, readdirSync, unlinkSync } from 'node:fs';
19
+ import { homedir, platform } from 'node:os';
20
+ import { join } from 'node:path';
21
+ import { LICENSE_TERMS_URL, SUPPORT_EMAIL } from './product-info.js';
22
+ export const DEFAULT_LOG_LEVEL = 'basic';
23
+ export const DEFAULT_LOG_KEEP_DAYS = 30;
24
+ /** Laengste Zeichenkette, die auch ausserhalb der Inhaltsfelder im Klartext bleibt. */
25
+ const MAX_PLAIN_STRING = 60;
26
+ /** Laengster Pfad, der im Klartext bleibt. */
27
+ const MAX_PATH_STRING = 300;
28
+ /** Hoechstzahl protokollierter Eintraege je Liste oder Objekt. */
29
+ const MAX_ITEMS = 20;
30
+ /**
31
+ * Parameter, deren Wert Dokumentinhalt sein kann. Ihr Wert wird immer
32
+ * ersetzt – bei Zeichenketten durch die Laengenangabe, bei Listen und
33
+ * Objekten durch die Zahl der Eintraege. Zahlen und Wahrheitswerte bleiben
34
+ * (z. B. `rows` als Zeilenzahl in create_table).
35
+ */
36
+ const CONTENT_KEYS = [
37
+ 'cells',
38
+ 'content',
39
+ 'find',
40
+ 'initial_text',
41
+ 'marker',
42
+ 'replace',
43
+ 'rows',
44
+ 'style_map',
45
+ 'template',
46
+ 'text',
47
+ 'text_after',
48
+ 'text_before',
49
+ 'values',
50
+ ];
51
+ function isContentKey(key) {
52
+ if (CONTENT_KEYS.includes(key))
53
+ return true;
54
+ return key.endsWith('_text') || key.endsWith('_texts');
55
+ }
56
+ /** Zeichen zaehlen, nicht Bytes. */
57
+ export function charCount(value) {
58
+ return Array.from(value).length;
59
+ }
60
+ function textNote(value) {
61
+ return '<Text, ' + String(charCount(value)) + ' Zeichen>';
62
+ }
63
+ /** Benutzerverzeichnis in einem Text durch "~" ersetzen. */
64
+ export function maskHome(value, home = homedir()) {
65
+ if (home.length < 2)
66
+ return value;
67
+ return value.split(home).join('~');
68
+ }
69
+ function looksLikePath(value) {
70
+ return value.includes('/') || value.includes('\\') || /^[A-Za-z]:/.test(value);
71
+ }
72
+ function filterValue(key, value, home, depth) {
73
+ if (value === null)
74
+ return null;
75
+ if (typeof value === 'number' || typeof value === 'boolean')
76
+ return value;
77
+ if (isContentKey(key)) {
78
+ if (typeof value === 'string')
79
+ return textNote(value);
80
+ if (Array.isArray(value))
81
+ return '<Liste, ' + String(value.length) + ' Eintraege>';
82
+ if (typeof value === 'object')
83
+ return '<Objekt, ' + String(Object.keys(value).length) + ' Felder>';
84
+ return '<Wert>';
85
+ }
86
+ if (typeof value === 'string') {
87
+ const masked = maskHome(value, home);
88
+ if (value.includes('\n'))
89
+ return textNote(value);
90
+ /*
91
+ * Pfade bleiben lesbar – ohne sie ist ein Ablagefehler nicht zu finden.
92
+ * Der Benutzername ist heraus, und eine Zeile ohne Umbruch, die einen
93
+ * Trenner enthaelt, ist kein Fliesstext.
94
+ */
95
+ if (looksLikePath(value))
96
+ return charCount(value) > MAX_PATH_STRING ? textNote(value) : masked;
97
+ if (charCount(value) > MAX_PLAIN_STRING)
98
+ return textNote(value);
99
+ return masked;
100
+ }
101
+ if (depth >= 3)
102
+ return '<verschachtelt>';
103
+ if (Array.isArray(value)) {
104
+ const shown = value.slice(0, MAX_ITEMS).map((item) => filterValue(key, item, home, depth + 1));
105
+ if (value.length > MAX_ITEMS)
106
+ shown.push('<' + String(value.length - MAX_ITEMS) + ' weitere>');
107
+ return shown;
108
+ }
109
+ if (typeof value === 'object') {
110
+ const record = value;
111
+ const out = {};
112
+ let count = 0;
113
+ for (const innerKey of Object.keys(record)) {
114
+ if (count >= MAX_ITEMS) {
115
+ out['…'] = '<' + String(Object.keys(record).length - MAX_ITEMS) + ' weitere>';
116
+ break;
117
+ }
118
+ out[innerKey] = filterValue(innerKey, record[innerKey], home, depth + 1);
119
+ count += 1;
120
+ }
121
+ return out;
122
+ }
123
+ return '<Wert>';
124
+ }
125
+ /** Aufrufsatz fuer das Protokoll entschaerfen (Stufe "basic"). */
126
+ export function filterArgs(args, home = homedir()) {
127
+ const out = {};
128
+ for (const key of Object.keys(args)) {
129
+ out[key] = filterValue(key, args[key], home, 0);
130
+ }
131
+ return out;
132
+ }
133
+ /** Protokollzeile fuer die Stufe "basic" aufbereiten. */
134
+ export function filterEntry(entry, home = homedir()) {
135
+ return {
136
+ ...entry,
137
+ args: filterArgs(entry.args, home),
138
+ error_message: entry.error_message === null ? null : maskHome(entry.error_message, home),
139
+ warnings: entry.warnings.map((warning) => maskHome(warning, home)),
140
+ };
141
+ }
142
+ export function readLogLevel(env = process.env) {
143
+ const raw = env['LAYOUTBRIDGE_SPM_LOG_LEVEL'];
144
+ if (raw === undefined)
145
+ return DEFAULT_LOG_LEVEL;
146
+ const value = raw.trim().toLowerCase();
147
+ if (value === 'off' || value === 'basic' || value === 'full')
148
+ return value;
149
+ return DEFAULT_LOG_LEVEL;
150
+ }
151
+ /** Aufbewahrungsfrist in Tagen; 0 schaltet das Aufraeumen ab. */
152
+ export function readLogKeepDays(env = process.env) {
153
+ const raw = env['LAYOUTBRIDGE_SPM_LOG_KEEP_DAYS'];
154
+ if (raw === undefined || raw.trim().length === 0)
155
+ return DEFAULT_LOG_KEEP_DAYS;
156
+ const value = Number(raw.trim());
157
+ if (!Number.isFinite(value) || value < 0)
158
+ return DEFAULT_LOG_KEEP_DAYS;
159
+ return Math.floor(value);
160
+ }
161
+ export function defaultLogDirectory() {
162
+ const fromEnv = process.env['LAYOUTBRIDGE_SPM_LOG_DIR'];
163
+ if (fromEnv !== undefined && fromEnv.length > 0)
164
+ return fromEnv;
165
+ if (platform() === 'darwin') {
166
+ return join(homedir(), 'Library', 'Logs', 'layoutbridge-spm');
167
+ }
168
+ return join(homedir(), '.layoutbridge-spm', 'logs');
169
+ }
170
+ function twoDigits(value) {
171
+ return String(value).padStart(2, '0');
172
+ }
173
+ /**
174
+ * Protokolldateien loeschen, die aelter als `keepDays` Tage sind. Das Datum
175
+ * kommt aus dem Dateinamen; gerechnet wird ab dem Ende des genannten Tages,
176
+ * damit nichts zu frueh verschwindet. Gibt die geloeschten Namen zurueck.
177
+ */
178
+ export function removeOldLogs(directory, keepDays, now = new Date()) {
179
+ if (keepDays <= 0)
180
+ return [];
181
+ const cutoff = now.getTime() - keepDays * 24 * 60 * 60 * 1000;
182
+ let names;
183
+ try {
184
+ names = readdirSync(directory);
185
+ }
186
+ catch {
187
+ return [];
188
+ }
189
+ const removed = [];
190
+ for (const name of names) {
191
+ const match = /^(\d{4})-(\d{2})-(\d{2})_pid\d+\.log$/.exec(name);
192
+ if (match === null)
193
+ continue;
194
+ const endOfDay = Date.parse(match[1] + '-' + match[2] + '-' + match[3] + 'T23:59:59.999Z');
195
+ if (Number.isNaN(endOfDay) || endOfDay >= cutoff)
196
+ continue;
197
+ try {
198
+ unlinkSync(join(directory, name));
199
+ removed.push(name);
200
+ }
201
+ catch {
202
+ /* schon weg oder nicht loeschbar – kein Grund fuer einen Abbruch */
203
+ }
204
+ }
205
+ return removed;
206
+ }
207
+ export class CallLogger {
208
+ file;
209
+ level;
210
+ removedLogs;
211
+ home;
212
+ broken = false;
213
+ constructor(directory = defaultLogDirectory(), now = new Date(), options = {}) {
214
+ this.level = options.level ?? readLogLevel();
215
+ this.home = options.home ?? homedir();
216
+ const stamp = now.getFullYear() +
217
+ '-' + twoDigits(now.getMonth() + 1) +
218
+ '-' + twoDigits(now.getDate());
219
+ this.file = join(directory, `${stamp}_pid${process.pid}.log`);
220
+ this.removedLogs = removeOldLogs(directory, options.keepDays ?? readLogKeepDays(), now);
221
+ if (this.level === 'off') {
222
+ this.broken = true;
223
+ return;
224
+ }
225
+ try {
226
+ mkdirSync(directory, { recursive: true });
227
+ }
228
+ catch (error) {
229
+ this.broken = true;
230
+ process.stderr.write(`[layoutbridge-spm] no log directory: ${String(error)}\n`);
231
+ return;
232
+ }
233
+ this.writeLine({
234
+ ts: new Date().toISOString(),
235
+ event: 'start',
236
+ log_level: this.level,
237
+ note: this.level === 'full'
238
+ ? 'Log level "full": tool arguments are written unfiltered, including document text and paths with your user name. Set LAYOUTBRIDGE_SPM_LOG_LEVEL=basic when the support case is closed.'
239
+ : 'Log level "basic": argument names with harmless values only; text is reduced to its length, paths hide the user name.',
240
+ removed_old_logs: this.removedLogs.length,
241
+ /* Lizenzbedingungen und Support-Adresse in der Startzeile des Protokolls (A-11 Nr. 3, A-10 Nr. 1). */
242
+ license_terms: LICENSE_TERMS_URL,
243
+ support: SUPPORT_EMAIL,
244
+ });
245
+ }
246
+ log(entry) {
247
+ if (this.broken)
248
+ return;
249
+ const prepared = this.level === 'full' ? entry : filterEntry(entry, this.home);
250
+ this.writeLine({ ts: new Date().toISOString(), ...prepared });
251
+ }
252
+ writeLine(record) {
253
+ if (this.broken)
254
+ return;
255
+ try {
256
+ appendFileSync(this.file, JSON.stringify(record) + '\n', 'utf8');
257
+ }
258
+ catch (error) {
259
+ this.broken = true;
260
+ process.stderr.write(`[layoutbridge-spm] cannot write log: ${String(error)}\n`);
261
+ }
262
+ }
263
+ }
264
+ /** Protokollsenke, die nichts tut – fuer Tests. */
265
+ export class NullLogger {
266
+ log() {
267
+ /* absichtlich leer */
268
+ }
269
+ }
270
+ //# sourceMappingURL=logging.js.map
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Feste Produktangaben, die in Meldungen und Protokoll erscheinen (Auftraege
3
+ * A-10 Nr. 1 und A-11 Nr. 1/3 des Vermarktungsprojekts, 17.09.2026; E-72).
4
+ *
5
+ * Die Adressen sind vom Vermarktungsprojekt festgelegt und aendern sich
6
+ * nicht mehr; die englische Fassung ist massgeblich fuer Verzeichnisse, die
7
+ * deutsche fuer Kunden. Keine Hauswerte, keine Umgebungsnamen – nur das,
8
+ * was jeder Kunde des Produkts sehen soll.
9
+ */
10
+ /** Support-Eingang (A-10 Nr. 1; seit A-12 Nr. 1 unter dem Dach MCP Interfaces, E-75). Erscheint im Klartext in jeder Meldung der Fehlerart `license`. */
11
+ export const SUPPORT_EMAIL = 'kontakt@mcp-interfaces.com';
12
+ /** Lizenzbedingungen (EULA), englisch (massgeblich fuer Verzeichnisse) und deutsch. */
13
+ export const LICENSE_TERMS_URL = 'https://mcp-interfaces.com/layoutbridge/license.html';
14
+ export const LICENSE_TERMS_URL_DE = 'https://mcp-schnittstellen.de/layoutbridge/lizenzbedingungen.html';
15
+ /** Datenschutzerklaerung des Produkts, englisch und deutsch; dazu die des Zahlungsdienstes. */
16
+ export const PRIVACY_POLICY_URL = 'https://mcp-interfaces.com/layoutbridge/privacy.html';
17
+ export const PRIVACY_POLICY_URL_DE = 'https://mcp-schnittstellen.de/layoutbridge/datenschutz.html';
18
+ /**
19
+ * Datenschutzerklaerung je Zahlungsdienst (A-15 Nr. 5, 23.09.2026). Zum
20
+ * Verkaufsstart gilt Polar (A-15a); lemonsqueezy bleibt als Rueckfall der
21
+ * Bauphase, ausgegeben wird der Eintrag des gewaehlten Dienstes.
22
+ */
23
+ export const PAYMENT_PRIVACY_POLICY_URLS = {
24
+ lemonsqueezy: 'https://www.lemonsqueezy.com/privacy',
25
+ polar: 'https://polar.sh/legal/privacy',
26
+ };
27
+ /**
28
+ * Der Eintrag des vorgegebenen Dienstes (polar seit A-15a Nr. 2; muss zu
29
+ * DEFAULT_LICENSE_PROVIDER in license/index.ts passen, ein Test wacht
30
+ * darueber). Steht in manifest.json und in der README.
31
+ */
32
+ export const PAYMENT_PRIVACY_POLICY_URL = PAYMENT_PRIVACY_POLICY_URLS['polar'];
33
+ /** Datenschutzerklaerung eines bestimmten Dienstes; unbekannt = Vorgabe. */
34
+ export function paymentPrivacyPolicyUrlFor(provider) {
35
+ return PAYMENT_PRIVACY_POLICY_URLS[provider] ?? PAYMENT_PRIVACY_POLICY_URL;
36
+ }
37
+ /** Produktseite. */
38
+ export const PRODUCT_PAGE_URL = 'https://mcp-interfaces.com/layoutbridge/';
39
+ export const PRODUCT_PAGE_URL_DE = 'https://mcp-schnittstellen.de/layoutbridge/';
40
+ /** Haengt den Support-Hinweis an eine Meldung, sofern die Adresse nicht schon darin steht. */
41
+ export function withSupportHint(message) {
42
+ if (message.includes(SUPPORT_EMAIL))
43
+ return message;
44
+ const trimmed = message.trimEnd();
45
+ const separator = /[.!?]$/.test(trimmed) ? ' ' : '. ';
46
+ return trimmed + separator + 'Support: ' + SUPPORT_EMAIL;
47
+ }
48
+ //# sourceMappingURL=product-info.js.map
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * layoutbridge-spm – MCP-Server für Adobe InDesign (bis 04.09.2026: indesign-connector-spm).
4
+ *
5
+ * Startet den Server ueber die Standardein-/-ausgabe (stdio) und meldet die
6
+ * Werkzeuge aus tools.ts an. Aufrufe laufen sequenziell (SerialTransport).
7
+ */
8
+ import { readFileSync } from 'node:fs';
9
+ import { homedir } from 'node:os';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
14
+ import { loadServerConfig } from './config.js';
15
+ import { CallLogger } from './logging.js';
16
+ import { runTool } from './dispatch.js';
17
+ import { createLicenseGate, runDeactivation } from './license/index.js';
18
+ import { LICENSE_TERMS_URL, LICENSE_TERMS_URL_DE, SUPPORT_EMAIL } from './product-info.js';
19
+ import { createTerminalIo, runSetup } from './setup.js';
20
+ import { TOOLS } from './tools.js';
21
+ import { nodeVersionProblem } from './start.cjs';
22
+ import { ComTransport } from '../transport/com.js';
23
+ import { OsascriptTransport } from '../transport/osascript.js';
24
+ import { SerialTransport } from '../transport/queue.js';
25
+ export const SERVER_NAME = 'layoutbridge-spm';
26
+ /**
27
+ * Version aus package.json – eine Quelle fuer Paket, Server, server.json und
28
+ * manifest.json (A-11 Nr. 6; scripts/version-sync.mjs zieht die beiden
29
+ * Dateien nach). Faellt auf 0.0.0 zurueck, wenn package.json nicht lesbar ist.
30
+ */
31
+ export const SERVER_VERSION = readPackageVersion();
32
+ function readPackageVersion() {
33
+ try {
34
+ const packageJson = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'package.json');
35
+ const parsed = JSON.parse(readFileSync(packageJson, 'utf8'));
36
+ return typeof parsed.version === 'string' ? parsed.version : '0.0.0';
37
+ }
38
+ catch {
39
+ return '0.0.0';
40
+ }
41
+ }
42
+ /**
43
+ * Haelt fest, ob noch ein Aufruf laeuft. Der Server darf sich erst beenden,
44
+ * wenn die Eingabe zu Ende ist UND keine Antwort mehr aussteht – sonst bricht
45
+ * er einen laufenden InDesign-Aufruf mitten im Satz ab.
46
+ */
47
+ export class Lifecycle {
48
+ onIdle;
49
+ inFlight = 0;
50
+ inputEnded = false;
51
+ stopping = false;
52
+ constructor(onIdle) {
53
+ this.onIdle = onIdle;
54
+ }
55
+ begin() {
56
+ this.inFlight += 1;
57
+ }
58
+ end() {
59
+ this.inFlight = Math.max(0, this.inFlight - 1);
60
+ this.check();
61
+ }
62
+ inputIsOver() {
63
+ this.inputEnded = true;
64
+ this.check();
65
+ }
66
+ get busy() {
67
+ return this.inFlight > 0;
68
+ }
69
+ check() {
70
+ if (this.inputEnded && this.inFlight === 0 && !this.stopping) {
71
+ this.stopping = true;
72
+ this.onIdle();
73
+ }
74
+ }
75
+ }
76
+ export function createServer(transport, logger, lifecycle, config = {}, gate) {
77
+ const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION });
78
+ for (const definition of TOOLS) {
79
+ server.registerTool(definition.name, {
80
+ title: definition.title,
81
+ description: definition.description,
82
+ inputSchema: definition.inputShape,
83
+ annotations: {
84
+ title: definition.title,
85
+ readOnlyHint: definition.readOnly,
86
+ destructiveHint: definition.destructive === true,
87
+ idempotentHint: definition.readOnly,
88
+ openWorldHint: false,
89
+ },
90
+ }, async (input) => {
91
+ lifecycle?.begin();
92
+ try {
93
+ /* Lizenzschranke (E-60): vor dem Transport, InDesign bleibt unberuehrt. */
94
+ let graceWarning = null;
95
+ if (gate !== undefined) {
96
+ const decision = await gate.decide(definition.name, definition.readOnly);
97
+ if (!decision.allowed) {
98
+ const refused = gate.toError(decision);
99
+ logger.log({
100
+ tool: definition.name,
101
+ command: definition.command,
102
+ args: {},
103
+ duration_ms: 0,
104
+ ok: false,
105
+ error_kind: 'license',
106
+ error_message: refused.error?.message ?? '',
107
+ warnings: [],
108
+ result_keys: [],
109
+ });
110
+ return {
111
+ content: [{ type: 'text', text: JSON.stringify(refused, null, 2) }],
112
+ isError: true,
113
+ };
114
+ }
115
+ graceWarning = decision.message;
116
+ }
117
+ let result = await runTool(definition, input, transport, logger, {
118
+ timeoutMs: definition.timeoutMs,
119
+ config,
120
+ });
121
+ if (graceWarning !== null) {
122
+ const warnings = Array.isArray(result.warnings) ? result.warnings : [];
123
+ result = { ...result, warnings: [...warnings, graceWarning] };
124
+ }
125
+ return {
126
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
127
+ isError: result.ok !== true,
128
+ };
129
+ }
130
+ finally {
131
+ lifecycle?.end();
132
+ }
133
+ });
134
+ }
135
+ return server;
136
+ }
137
+ /**
138
+ * Startet den Server ueber die Standardein-/-ausgabe und kuemmert sich um ein
139
+ * sauberes Ende: Ende der Eingabe merken, aber erst aufhoeren, wenn die letzte
140
+ * Antwort geschrieben ist.
141
+ */
142
+ export async function startStdioServer(transport, logger, config = {}, gate) {
143
+ let stopping = false;
144
+ const stop = (code) => {
145
+ if (stopping)
146
+ return;
147
+ stopping = true;
148
+ void transport.close().finally(() => {
149
+ /* Erst schreiben lassen, dann gehen. */
150
+ process.stdout.write('', () => process.exit(code));
151
+ });
152
+ };
153
+ const lifecycle = new Lifecycle(() => stop(0));
154
+ const server = createServer(transport, logger, lifecycle, config, gate);
155
+ process.stdin.on('end', () => lifecycle.inputIsOver());
156
+ process.stdin.on('close', () => lifecycle.inputIsOver());
157
+ server.server.onclose = () => lifecycle.inputIsOver();
158
+ process.on('SIGINT', () => stop(0));
159
+ process.on('SIGTERM', () => stop(0));
160
+ await server.connect(new StdioServerTransport());
161
+ return server;
162
+ }
163
+ /**
164
+ * Rechnerwechsel (A-6 Ergaenzung Nr. 7): `layoutbridge-spm --deactivate`
165
+ * gibt die Aktivierung dieses Arbeitsplatzes beim Dienst frei. Laeuft ohne
166
+ * InDesign und ohne Client; kein Werkzeug im Katalog (A-5).
167
+ */
168
+ async function deactivateFromCommandLine() {
169
+ const result = await runDeactivation();
170
+ process.stderr.write(`[${SERVER_NAME}] ${result.message}\n`);
171
+ process.exit(result.ok ? 0 : 1);
172
+ }
173
+ /**
174
+ * Einrichtung (A-13 Nr. 1, E-76): `layoutbridge-spm --setup` traegt den
175
+ * Server in Claude Desktop ein und legt den Lizenzschluessel ab. Nur auf
176
+ * ausdrueckliche Eingabe, kein Werkzeug im Katalog (A-5); laeuft vor
177
+ * Protokoll, Transport und Lizenzschranke, also ohne Netz und ohne InDesign.
178
+ */
179
+ async function setupFromCommandLine() {
180
+ const code = await runSetup(process.argv.slice(2), createTerminalIo(), {
181
+ platform: process.platform,
182
+ home: homedir(),
183
+ env: process.env,
184
+ now: new Date(),
185
+ nodeVersion: process.version,
186
+ });
187
+ process.exit(code);
188
+ }
189
+ /**
190
+ * Transport nach Plattform. Die einzige Stelle im Server, die zwischen den
191
+ * Wegen unterscheidet; alles darueber kennt nur die Schnittstelle.
192
+ * Der Windows-Weg ist gebaut, aber noch nicht an einem laufenden InDesign
193
+ * nachgewiesen (Vermerk E-67).
194
+ */
195
+ function createPlatformTransport() {
196
+ return process.platform === 'win32' ? new ComTransport() : new OsascriptTransport();
197
+ }
198
+ /** Eine Zeile mit Lizenzbedingungen und Support-Adresse fuer Startmeldung und Protokoll. */
199
+ export function describeProductTerms() {
200
+ return `licence terms: ${LICENSE_TERMS_URL} (de: ${LICENSE_TERMS_URL_DE}), support: ${SUPPORT_EMAIL}`;
201
+ }
202
+ async function main() {
203
+ if (process.argv.includes('--setup')) {
204
+ await setupFromCommandLine();
205
+ return;
206
+ }
207
+ if (process.argv.includes('--deactivate')) {
208
+ await deactivateFromCommandLine();
209
+ return;
210
+ }
211
+ const logger = new CallLogger();
212
+ const transport = new SerialTransport(createPlatformTransport());
213
+ const loaded = loadServerConfig();
214
+ const gate = createLicenseGate({ product: SERVER_NAME, version: SERVER_VERSION });
215
+ process.stderr.write(`[${SERVER_NAME}] ${SERVER_VERSION} started, ${TOOLS.length} tools, transport: ${transport.name}, log: ${logger.file} (level ${logger.level}${logger.removedLogs.length > 0 ? `, ${logger.removedLogs.length} old log file(s) removed` : ''}), config: ${loaded.path ?? 'none'}\n`);
216
+ if (logger.level === 'full') {
217
+ process.stderr.write(`[${SERVER_NAME}] log level "full": tool arguments are written unfiltered, including document text and paths with your user name.\n`);
218
+ }
219
+ for (const problem of loaded.problems) {
220
+ process.stderr.write(`[${SERVER_NAME}] config: ${problem}\n`);
221
+ }
222
+ process.stderr.write(`[${SERVER_NAME}] ${gate.describe()}\n`);
223
+ /* Lizenzbedingungen und Support in der Startzeile (A-11 Nr. 3, A-10 Nr. 1). */
224
+ process.stderr.write(`[${SERVER_NAME}] ${describeProductTerms()}\n`);
225
+ await startStdioServer(transport, logger, loaded.config, gate);
226
+ }
227
+ /**
228
+ * Einstieg fuer die Kommandozeile. Aufgerufen von der Startdatei `start.cjs`
229
+ * (bin, MCPB-Bundle; prueft vorher die Node-Version, E-74) und beim direkten
230
+ * Start `node dist/src/mcp/server.js` (Handeintrag, bestehende Konfigurationen).
231
+ */
232
+ export function runCli() {
233
+ /* Beim direkten Start von server.js hat keine Startdatei geprueft. Laufen die
234
+ Imports unter Node 18/19 noch durch, soll die Meldung trotzdem die richtige
235
+ Ursache nennen (E-74). */
236
+ const problem = nodeVersionProblem(process.version);
237
+ if (problem !== null) {
238
+ process.stderr.write(problem);
239
+ process.exit(1);
240
+ }
241
+ main().catch((error) => {
242
+ process.stderr.write(`[${SERVER_NAME}] fatal: ${String(error)}\n`);
243
+ process.exit(1);
244
+ });
245
+ }
246
+ const isEntryPoint = process.argv[1] !== undefined && process.argv[1].endsWith('server.js');
247
+ if (isEntryPoint) {
248
+ runCli();
249
+ }
250
+ //# sourceMappingURL=server.js.map