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.
- package/CHANGELOG.md +171 -0
- package/LICENSE +26 -0
- package/README.de.md +318 -0
- package/README.md +304 -4
- package/THIRD-PARTY-NOTICES.md +2878 -0
- package/dist/src/mcp/config.js +125 -0
- package/dist/src/mcp/dispatch.js +123 -0
- package/dist/src/mcp/license/free-tools.js +16 -0
- package/dist/src/mcp/license/gate.js +209 -0
- package/dist/src/mcp/license/index.js +96 -0
- package/dist/src/mcp/license/instance-name.js +56 -0
- package/dist/src/mcp/license/key-source.js +58 -0
- package/dist/src/mcp/license/lemonsqueezy-verifier.js +207 -0
- package/dist/src/mcp/license/placeholder-verifier.js +10 -0
- package/dist/src/mcp/license/polar-ids.js +63 -0
- package/dist/src/mcp/license/polar-verifier.js +265 -0
- package/dist/src/mcp/license/product-ids.js +72 -0
- package/dist/src/mcp/license/state-store.js +91 -0
- package/dist/src/mcp/license/types.js +11 -0
- package/dist/src/mcp/logging.js +270 -0
- package/dist/src/mcp/product-info.js +48 -0
- package/dist/src/mcp/server.js +250 -0
- package/dist/src/mcp/setup.js +608 -0
- package/dist/src/mcp/start.cjs +95 -0
- package/dist/src/mcp/tool-definition.js +2 -0
- package/dist/src/mcp/tools.js +2004 -0
- package/dist/src/mcp/typography-rules.js +70 -0
- package/dist/src/transport/com.js +250 -0
- package/dist/src/transport/osascript.js +171 -0
- package/dist/src/transport/queue.js +19 -0
- package/dist/src/transport/script-bundle.js +92 -0
- package/dist/src/transport/transport.js +30 -0
- package/indesign/commands/10-app.js +6 -0
- package/indesign/commands/20-documents.js +513 -0
- package/indesign/commands/22-document-setup.js +118 -0
- package/indesign/commands/23-save-package.js +255 -0
- package/indesign/commands/25-export.js +379 -0
- package/indesign/commands/26-export-image.js +197 -0
- package/indesign/commands/27-textdefaults.js +151 -0
- package/indesign/commands/28-selection.js +174 -0
- package/indesign/commands/30-pages.js +1032 -0
- package/indesign/commands/31-view.js +210 -0
- package/indesign/commands/32-sections.js +203 -0
- package/indesign/commands/35-parents.js +428 -0
- package/indesign/commands/37-layout.js +760 -0
- package/indesign/commands/40-frames.js +836 -0
- package/indesign/commands/41-shapes.js +505 -0
- package/indesign/commands/42-groups.js +117 -0
- package/indesign/commands/43-layers.js +291 -0
- package/indesign/commands/44-effects.js +274 -0
- package/indesign/commands/45-frame-options.js +247 -0
- package/indesign/commands/47-anchored.js +287 -0
- package/indesign/commands/50-modify.js +781 -0
- package/indesign/commands/52-align.js +139 -0
- package/indesign/commands/55-fit.js +106 -0
- package/indesign/commands/60-images.js +771 -0
- package/indesign/commands/62-embed.js +120 -0
- package/indesign/commands/69-placeholders.js +127 -0
- package/indesign/commands/70-text.js +567 -0
- package/indesign/commands/71-outlines.js +92 -0
- package/indesign/commands/72-place-text.js +366 -0
- package/indesign/commands/74-stories.js +116 -0
- package/indesign/commands/75-tables.js +676 -0
- package/indesign/commands/76-text-variables.js +260 -0
- package/indesign/commands/77-story-options.js +86 -0
- package/indesign/commands/78-frame-lines.js +304 -0
- package/indesign/commands/79-check-typography.js +303 -0
- package/indesign/commands/80-styles.js +1620 -0
- package/indesign/commands/81-composition.js +355 -0
- package/indesign/commands/82-object-styles.js +263 -0
- package/indesign/commands/83-indents-tabs.js +237 -0
- package/indesign/commands/84-import-styles.js +142 -0
- package/indesign/commands/85-swatches.js +112 -0
- package/indesign/commands/86-gradients.js +185 -0
- package/indesign/commands/87-replace-swatch.js +180 -0
- package/indesign/commands/88-replace-font.js +217 -0
- package/indesign/interpreter.js +758 -0
- package/package.json +55 -6
- 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
|