hostinger-api-mcp 3.23.1 → 3.23.2

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 CHANGED
@@ -4,6 +4,13 @@ Generated at release time from the diff between the previous and current
4
4
  build. Tool changes are listed individually; everything else is summarised
5
5
  by area.
6
6
 
7
+ ## v3.23.2 — 2026-10-30
8
+
9
+ ### Also in this release
10
+
11
+ - Bundled agent skills: 4 files
12
+ - Other files: `src/core/skills.js`, `src/core/skills.ts`
13
+
7
14
  ## v3.23.1 — 2026-10-30
8
15
 
9
16
  ### Also in this release
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostinger-api-mcp",
3
- "version": "3.23.1",
3
+ "version": "3.23.2",
4
4
  "description": "Hostinger API — manage domains, DNS, hosting, VPS, billing and more from Gemini CLI.",
5
5
  "mcpServers": {
6
6
  "hostinger": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hostinger-api-mcp",
3
- "version": "3.23.1",
3
+ "version": "3.23.2",
4
4
  "mcpName": "io.github.hostinger/hostinger-api-mcp",
5
5
  "description": "MCP server for Hostinger API",
6
6
  "repository": {
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/hostinger/api-mcp-server",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.23.1",
9
+ "version": "3.23.2",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@hostinger/mcp",
14
- "version": "3.23.1",
14
+ "version": "3.23.2",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },
@@ -198,6 +198,7 @@ took from it.
198
198
  | File (under `~/.hostinger/<domain>/`) | Written by | Contains |
199
199
  |---|---|---|
200
200
  | `state.json` | every stage | tool, next stage, Hostinger connector version, timestamps |
201
+ | `run-log.json` | `run-log.mjs` only | the current run, each stage's start and end times, which reports reached Hostinger |
201
202
  | `connect.json` | connect | tool, MCP version, sign-in method, plan; shape in `connect/SKILL.md` |
202
203
  | `brief.md` | refine | refined brief, audience, tone, pages, must-haves |
203
204
  | `site.json` | provision | chosen backend, username, install id, all URLs |
@@ -234,12 +235,11 @@ provision runs in the background, its status lives in its own `provision` field
234
235
  prepare step ignores `provision` entirely.
235
236
  - On **resume**, `provision.status` is a hint only: re-poll `hosting_websites_list-setups` to learn
236
237
  the truth, then update the field.
237
- - `run` (the current run), `history` (one entry per stage or phase, with real start and end
238
- times), and `reports_sent` / `reports_rejected` (which copy of each report reached Hostinger) are
239
- written **only** by `run-log.mjs`. Leave them alone when you update `state.json` by hand.
238
+ - Write `state.json` however is easiest, rewriting the whole file included: the run log lives in
239
+ `run-log.json`, which only `run-log.mjs` writes.
240
240
  - The **onboarding slug** is how hPanel names the coding tool: `claude` for Claude Code, else
241
241
  `cursor`, `codex`, `vscode`, `antigravity`, `devin`. Map it from the `<tool>` you pass to
242
- `run-start --tool` (`state.json → run.tool`, e.g. `claude-code` → `claude`).
242
+ `run-start --tool` (`run-log.json → run.tool`, e.g. `claude-code` → `claude`).
243
243
 
244
244
  ---
245
245
 
@@ -76,20 +76,43 @@ function workspace(domain) {
76
76
  dir,
77
77
  domain,
78
78
  statePath: join(dir, 'state.json'),
79
+ logPath: join(dir, 'run-log.json'),
79
80
  eventsPath: join(dir, 'events.jsonl'),
80
81
  reportsDir: join(dir, 'reports'),
81
82
  };
82
83
  }
83
84
 
84
- const loadState = (ws) => readJson(ws.statePath, {});
85
- function saveState(ws, state) {
86
- state.updated_at = now();
85
+ // state.json belongs to the agent, which may rewrite it whole or leave it half-written, so the run
86
+ // log keeps its own file and only reads state.json, treating anything unreadable as empty.
87
+ function readState(ws) {
88
+ try {
89
+ const state = readJson(ws.statePath, {});
90
+ return state && typeof state === 'object' && !Array.isArray(state) ? state : {};
91
+ } catch {
92
+ return {};
93
+ }
94
+ }
95
+
96
+ function loadLog(ws) {
97
+ if (!existsSync(ws.logPath)) migrate(ws);
98
+ return readJson(ws.logPath, {});
99
+ }
100
+ const saveLog = (ws, log) => writeJson(ws.logPath, log);
101
+
102
+ // Builds started before run-log.json existed kept the run log in state.json: move it out once, so
103
+ // those builds keep their timings and the record of reports already sent.
104
+ function migrate(ws) {
105
+ const state = readState(ws);
106
+ const fields = ['run', 'history', 'reports_sent', 'reports_rejected'].filter((key) => key in state);
107
+ if (!fields.length) return;
108
+ saveLog(ws, Object.fromEntries(fields.map((key) => [key, state[key]])));
109
+ for (const key of fields) delete state[key];
87
110
  writeJson(ws.statePath, state);
88
111
  }
89
112
 
90
- function currentRun(ws, state) {
91
- if (!state.run?.id) fail('no run started; call run-start first (check-state does this)');
92
- return state.run;
113
+ function currentRun(log) {
114
+ if (!log.run?.id) fail('no run started; call run-start first (check-state does this)');
115
+ return log.run;
93
116
  }
94
117
 
95
118
  function readEvents(ws) {
@@ -110,14 +133,14 @@ function readConnect(ws) {
110
133
  }
111
134
 
112
135
  function runStart(ws, flags) {
113
- const state = loadState(ws);
136
+ const log = loadLog(ws);
114
137
  // A previous run that never reached its report (closed window, crash) is closed out first, so
115
138
  // its log still turns into a report.
116
- if (state.run?.id && !state.run.reported_at) {
139
+ if (log.run?.id && !log.run.reported_at) {
117
140
  buildReport(ws, { outcome: 'abandoned' });
118
- console.log(`closed previous run ${state.run.id} as abandoned`);
141
+ console.log(`closed previous run ${log.run.id} as abandoned`);
119
142
  }
120
- const fresh = loadState(ws);
143
+ const fresh = loadLog(ws);
121
144
  const previous = fresh.run ?? {};
122
145
  const startedAt = now();
123
146
  const connect = readConnect(ws);
@@ -135,21 +158,21 @@ function runStart(ws, flags) {
135
158
  id,
136
159
  kind,
137
160
  started_at: startedAt,
138
- tool: flags.tool ?? connect.tool ?? fresh.tool ?? previous.tool ?? null,
161
+ tool: flags.tool ?? connect.tool ?? readState(ws).tool ?? previous.tool ?? null,
139
162
  model: flags.model ?? previous.model ?? null,
140
163
  skill: flags.skill ?? previous.skill ?? null,
141
164
  };
142
165
  fresh.history ??= [];
143
- saveState(ws, fresh);
166
+ saveLog(ws, fresh);
144
167
  console.log(`run ${fresh.run.id} started`);
145
168
  }
146
169
 
147
170
  function runUpdate(ws, flags) {
148
- const state = loadState(ws);
149
- const run = currentRun(ws, state);
171
+ const log = loadLog(ws);
172
+ const run = currentRun(log);
150
173
  for (const key of ['kind', 'tool', 'model', 'skill']) if (typeof flags[key] === 'string') run[key] = flags[key];
151
174
  if (!['build', 'resume', 'redesign', 'iterate'].includes(run.kind)) fail(`unknown kind "${run.kind}"; one of build, resume, redesign, iterate`);
152
- saveState(ws, state);
175
+ saveLog(ws, log);
153
176
  console.log(`run ${run.id} updated`);
154
177
  }
155
178
 
@@ -157,8 +180,8 @@ function runUpdate(ws, flags) {
157
180
  // to the run it follows: reopen it, so the next `report` rewrites that run's report instead of
158
181
  // starting a second one.
159
182
  function runResume(ws, flags) {
160
- const state = loadState(ws);
161
- const run = currentRun(ws, state);
183
+ const log = loadLog(ws);
184
+ const run = currentRun(log);
162
185
  if (!run.reported_at) {
163
186
  console.log(`run ${run.id} is still open; nothing to resume`);
164
187
  return;
@@ -168,42 +191,42 @@ function runResume(ws, flags) {
168
191
  { resumed_at: now(), after_report_at: run.reported_at, reason: typeof flags.reason === 'string' ? flags.reason : null },
169
192
  ];
170
193
  delete run.reported_at;
171
- saveState(ws, state);
194
+ saveLog(ws, log);
172
195
  console.log(`run ${run.id} resumed (follow-up ${run.follow_ups.length})`);
173
196
  }
174
197
 
175
198
  function stageStart(ws, [stage], flags) {
176
199
  if (!STAGES.includes(stage)) fail(`unknown stage "${stage}"; one of ${STAGES.join(', ')}`);
177
- const state = loadState(ws);
178
- const run = currentRun(ws, state);
179
- state.history ??= [];
200
+ const log = loadLog(ws);
201
+ const run = currentRun(log);
202
+ log.history ??= [];
180
203
  const phase = flags.phase ?? null;
181
- const open = state.history.find((h) => h.run === run.id && h.stage === stage && h.phase === phase && !h.ended_at);
204
+ const open = log.history.find((h) => h.run === run.id && h.stage === stage && h.phase === phase && !h.ended_at);
182
205
  if (open) {
183
206
  console.log(`${stage}${phase ? `/${phase}` : ''} already started at ${open.started_at}`);
184
207
  return;
185
208
  }
186
209
  const late = lateStart(flags, run);
187
- state.history.push({ run: run.id, stage, phase, started_at: late ?? now(), ended_at: null, result: null, note: null, ...(late ? { start_estimated: true } : {}) });
188
- saveState(ws, state);
210
+ log.history.push({ run: run.id, stage, phase, started_at: late ?? now(), ended_at: null, result: null, note: null, ...(late ? { start_estimated: true } : {}) });
211
+ saveLog(ws, log);
189
212
  console.log(`${stage}${phase ? `/${phase}` : ''} started${late ? ` (estimated start ${late})` : ''}`);
190
213
  }
191
214
 
192
215
  function stageEnd(ws, [stage, result], flags) {
193
216
  if (!STAGES.includes(stage)) fail(`unknown stage "${stage}"`);
194
217
  if (!RESULTS.includes(result)) fail(`unknown result "${result}"; one of ${RESULTS.join(', ')}`);
195
- const state = loadState(ws);
196
- const run = currentRun(ws, state);
197
- state.history ??= [];
218
+ const log = loadLog(ws);
219
+ const run = currentRun(log);
220
+ log.history ??= [];
198
221
  const phase = flags.phase ?? null;
199
- let entry = [...state.history].reverse().find((h) => h.run === run.id && h.stage === stage && h.phase === phase && !h.ended_at);
222
+ let entry = [...log.history].reverse().find((h) => h.run === run.id && h.stage === stage && h.phase === phase && !h.ended_at);
200
223
  if (!entry) {
201
224
  // Missed stage-start: use the remembered start if given, else the end of the previous stage; either way estimated.
202
- const runEntries = state.history.filter((h) => h.run === run.id);
225
+ const runEntries = log.history.filter((h) => h.run === run.id);
203
226
  const lastEnd = runEntries.map((h) => h.ended_at).filter(Boolean).sort().at(-1);
204
227
  const late = lateStart(flags, run);
205
228
  entry = { run: run.id, stage, phase, started_at: late ?? lastEnd ?? run.started_at, ended_at: null, result: null, note: null, start_estimated: true };
206
- state.history.push(entry);
229
+ log.history.push(entry);
207
230
  console.log(`${stage}${phase ? `/${phase}` : ''}: stage-start was not logged; start estimated as ${entry.started_at}${late ? '' : ' (end of the previous stage). Pass --started-ago if you know better'}`);
208
231
  } else if (flags['started-at'] || flags['started-ago']) {
209
232
  entry.started_at = lateStart(flags, run);
@@ -212,23 +235,23 @@ function stageEnd(ws, [stage, result], flags) {
212
235
  entry.ended_at = now();
213
236
  entry.result = result;
214
237
  if (typeof flags.note === 'string') entry.note = flags.note;
215
- saveState(ws, state);
238
+ saveLog(ws, log);
216
239
  console.log(`${stage}${phase ? `/${phase}` : ''} ended: ${result}`);
217
240
  }
218
241
 
219
242
  function stageFix(ws, [stage], flags) {
220
243
  if (!STAGES.includes(stage)) fail(`unknown stage "${stage}"`);
221
- const state = loadState(ws);
222
- const run = currentRun(ws, state);
244
+ const log = loadLog(ws);
245
+ const run = currentRun(log);
223
246
  const phase = flags.phase ?? null;
224
- const entry = [...(state.history ?? [])].reverse().find((h) => h.run === run.id && h.stage === stage && h.phase === phase);
247
+ const entry = [...(log.history ?? [])].reverse().find((h) => h.run === run.id && h.stage === stage && h.phase === phase);
225
248
  if (!entry) fail(`${stage}${phase ? `/${phase}` : ''} was never logged in this run; use stage-start with --started-ago instead`);
226
249
  const late = lateStart(flags, run);
227
250
  if (!late) fail('stage-fix needs --started-ago (e.g. 6m) or --started-at <ISO time>');
228
251
  if (entry.ended_at && late > entry.ended_at) fail(`the new start ${late} is after the stage ended (${entry.ended_at})`);
229
252
  entry.started_at = late;
230
253
  entry.start_estimated = true;
231
- saveState(ws, state);
254
+ saveLog(ws, log);
232
255
  console.log(`${stage}${phase ? `/${phase}` : ''} start set to ${late} (estimated)`);
233
256
  }
234
257
 
@@ -253,18 +276,16 @@ function event(ws, [type, raw]) {
253
276
  }
254
277
  // A bad line in events.jsonl would fail every later report and run-start.
255
278
  validate({ ...data, logged_at: now() }, eventSchema(type), `event ${type}`);
256
- const state = loadState(ws);
257
- const run = currentRun(ws, state);
279
+ const log = loadLog(ws);
280
+ const run = currentRun(log);
258
281
  appendFileSync(ws.eventsPath, `${JSON.stringify({ ts: now(), run: run.id, type, ...data })}\n`);
259
282
  console.log(`${type} logged`);
260
283
  }
261
284
 
262
285
  const minutesBetween = (a, b) => (a && b ? Math.round(((Date.parse(b) - Date.parse(a)) / 60000) * 10) / 10 : null);
263
286
 
264
- function frontendStack(dir) {
265
- // Framework is recorded in state.json by the frontend step.
266
- const state = readJson(join(dir, 'state.json'), {});
267
- const fe = state.frontend ?? {};
287
+ // Framework is recorded in state.json by the frontend step.
288
+ function frontendStack(fe, dir) {
268
289
  const framework = fe.framework ?? fe.stack ?? null;
269
290
  if (framework) return fe.rendering ? `${framework} (${fe.rendering})` : framework;
270
291
  // Fallback: an older build may have a design.md with a "## Stack" heading.
@@ -273,12 +294,13 @@ function frontendStack(dir) {
273
294
  }
274
295
 
275
296
  function buildReport(ws, { outcome, tokens } = {}) {
276
- const state = loadState(ws);
277
- const run = currentRun(ws, state);
297
+ const log = loadLog(ws);
298
+ const run = currentRun(log);
278
299
  const site = readJson(join(ws.dir, 'site.json'), {});
279
300
  const deploy = readJson(join(ws.dir, 'deploy.json'), {});
301
+ const state = readState(ws);
280
302
  const connect = readConnect(ws);
281
- const history = (state.history ?? []).filter((h) => h.run === run.id);
303
+ const history = (log.history ?? []).filter((h) => h.run === run.id);
282
304
  // Lines logged before events were checked on write may not match the schema.
283
305
  const dropped = [];
284
306
  const events = readEvents(ws)
@@ -316,7 +338,7 @@ function buildReport(ws, { outcome, tokens } = {}) {
316
338
  const report = {
317
339
  schema_version: '1',
318
340
  generated_at: now(),
319
- // Intent, not delivery: delivery is tracked in state.json.
341
+ // Intent, not delivery: delivery is tracked in run-log.json.
320
342
  consent: { sent: true, note: 'Sent to Hostinger automatically at the end of the run.' },
321
343
  run: { id: run.id, kind: run.kind, skill: run.skill, follow_ups: run.follow_ups ?? [] },
322
344
  info: {
@@ -327,9 +349,9 @@ function buildReport(ws, { outcome, tokens } = {}) {
327
349
  node: process.version,
328
350
  mcp: connect.mcp,
329
351
  backend: site.backend ?? null,
330
- frontend: frontendStack(ws.dir),
352
+ frontend: frontendStack(state.frontend ?? {}, ws.dir),
331
353
  outcome: resolvedOutcome,
332
- stopped_at: resolvedOutcome === 'live' ? null : stoppedAt(stages, state),
354
+ stopped_at: resolvedOutcome === 'live' ? null : stoppedAt(stages, state.stage),
333
355
  live_url: resolvedOutcome === 'live' ? deploy.urls?.site ?? site.urls?.site ?? null : null,
334
356
  admin_url: site.urls?.wp_admin ?? null,
335
357
  },
@@ -357,23 +379,23 @@ function buildReport(ws, { outcome, tokens } = {}) {
357
379
  writeFileSync(join(ws.reportsDir, `${run.id}.html`), renderHtml(report));
358
380
  copyFileSync(join(ws.reportsDir, `${run.id}.html`), join(ws.dir, 'report.html'));
359
381
 
360
- const fresh = loadState(ws);
382
+ const fresh = loadLog(ws);
361
383
  fresh.run.reported_at = now();
362
- saveState(ws, fresh);
384
+ saveLog(ws, fresh);
363
385
  for (const g of gaps) console.log(`warning: ${g.stage}${g.phase ? `/${g.phase}` : ''}: ${g.problem}`);
364
386
  return join(ws.dir, 'report.html');
365
387
  }
366
388
 
367
389
  // A report counts as sent only while the sent copy's generated_at matches the file.
368
390
  function reportFile(ws, runId) {
369
- const id = runId ?? currentRun(ws, loadState(ws)).id;
391
+ const id = runId ?? currentRun(loadLog(ws)).id;
370
392
  const file = join(ws.reportsDir, `${id}.json`);
371
393
  if (!existsSync(file)) fail(`no report for run ${id} at ${file}`);
372
394
  return { id, file, generatedAt: readJson(file, {}).generated_at };
373
395
  }
374
396
 
375
397
  function unsent(ws) {
376
- const state = loadState(ws);
398
+ const log = loadLog(ws);
377
399
  const files = existsSync(ws.reportsDir) ? readdirSync(ws.reportsDir).filter((f) => f.endsWith('.json')).sort() : [];
378
400
  // Older reports promised "nothing is uploaded".
379
401
  const pending = files
@@ -382,26 +404,26 @@ function unsent(ws) {
382
404
  return { id: f.replace(/\.json$/, ''), file: join(ws.reportsDir, f), generatedAt: report.generated_at, consent: report.consent };
383
405
  })
384
406
  .filter(({ consent }) => consent?.sent !== false)
385
- .filter(({ id, generatedAt }) => state.reports_sent?.[id] !== generatedAt && state.reports_rejected?.[id] !== generatedAt);
407
+ .filter(({ id, generatedAt }) => log.reports_sent?.[id] !== generatedAt && log.reports_rejected?.[id] !== generatedAt);
386
408
  if (!pending.length) console.log('no unsent reports');
387
409
  for (const { id, file } of pending) console.log(`${id} ${file}`);
388
410
  }
389
411
 
390
412
  function reportSent(ws, [runId]) {
391
413
  const { id, generatedAt } = reportFile(ws, runId);
392
- const state = loadState(ws);
393
- state.reports_sent = { ...state.reports_sent, [id]: generatedAt };
394
- saveState(ws, state);
414
+ const log = loadLog(ws);
415
+ log.reports_sent = { ...log.reports_sent, [id]: generatedAt };
416
+ saveLog(ws, log);
395
417
  console.log(`report ${id} marked sent`);
396
418
  }
397
419
 
398
420
  // A 422 is schema drift, so resending won't help.
399
421
  function reportRejected(ws, [runId]) {
400
422
  const { id, generatedAt } = reportFile(ws, runId);
401
- const state = loadState(ws);
402
- state.reports_rejected = { ...state.reports_rejected, [id]: generatedAt };
403
- saveState(ws, state);
404
- const run = currentRun(ws, state);
423
+ const log = loadLog(ws);
424
+ log.reports_rejected = { ...log.reports_rejected, [id]: generatedAt };
425
+ saveLog(ws, log);
426
+ const run = currentRun(log);
405
427
  const symptom = `build report ${id} rejected with 422 by hosting_headless_submit-build-report`;
406
428
  appendFileSync(ws.eventsPath, `${JSON.stringify({ ts: now(), run: run.id, type: 'issue', stage: run.reported_at ? 'done' : 'check_state', symptom, severity: 'medium', resolved: false, user_had_to_intervene: false })}\n`);
407
429
  console.log(`report ${id} marked rejected; issue logged in run ${run.id}`);
@@ -430,9 +452,9 @@ function loggingGaps(stages, run, outcome, backend) {
430
452
  return gaps;
431
453
  }
432
454
 
433
- function stoppedAt(stages, state) {
455
+ function stoppedAt(stages, fallbackStage) {
434
456
  const stuck = [...stages].reverse().find((s) => !s.ended_at || s.result === 'failed' || s.result === 'stopped');
435
- return stuck?.stage ?? state.stage ?? null;
457
+ return stuck?.stage ?? fallbackStage ?? null;
436
458
  }
437
459
 
438
460
  // Minimal JSON Schema subset, so reports from different tools share one shape without a dependency.
@@ -444,8 +444,8 @@ The template's token list is the **minimum**: add what the design needs (`--colo
444
444
  listed in *Grid, shape & motion* as plain px values.
445
445
 
446
446
  Then update `state.json`: set `"stage": "frontend"` (also for a redesign: restyle, then validate and
447
- deploy) and `updated_at` to the current UTC time (ISO 8601). Don't touch `provision`, `run` or
448
- `history` (`run-log.mjs` owns those), or add other keys.
447
+ deploy) and `updated_at` to the current UTC time (ISO 8601). Don't touch `provision` or add
448
+ other keys.
449
449
 
450
450
  If content prepare hasn't sourced images yet, it now follows `design.md` → **Imagery** (see
451
451
  `04-content-model/SKILL.md`).
@@ -129,7 +129,7 @@ open "$URL" # macOS
129
129
  ## Build report
130
130
 
131
131
  Right after the final report, save the build report for this run. It turns the run log (stage times
132
- from `state.json`, events from `events.jsonl`) into `reports/<run>.json` and `.html`, and copies the
132
+ from `run-log.json`, events from `events.jsonl`) into `reports/<run>.json` and `.html`, and copies the
133
133
  HTML to `report.html` in the workspace:
134
134
 
135
135
  ```bash