@displaydev/cli 0.19.0 → 0.20.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.
@@ -1,7 +1,4 @@
1
- /**
2
- * HTTP client for the display.dev API.
3
- * Used by the stdio MCP server to proxy tool calls to the REST API.
4
- */ function _assert_this_initialized(self) {
1
+ function _assert_this_initialized(self) {
5
2
  if (self === void 0) {
6
3
  throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
7
4
  }
@@ -284,6 +281,10 @@ function _ts_generator(thisArg, body) {
284
281
  };
285
282
  }
286
283
  }
284
+ /**
285
+ * HTTP client for the display.dev API.
286
+ * Used by the stdio MCP server to proxy tool calls to the REST API.
287
+ */ import { readLatestVersionFromResponse } from './update-notice.js';
287
288
  export var ApiError = /*#__PURE__*/ function(Error1) {
288
289
  "use strict";
289
290
  _inherits(ApiError, Error1);
@@ -311,15 +312,44 @@ export var ApiClient = /*#__PURE__*/ function() {
311
312
  "use strict";
312
313
  function ApiClient(config) {
313
314
  _class_call_check(this, ApiClient);
314
- var _config_clientType;
315
+ var _config_clientType, _config_version;
315
316
  _define_property(this, "baseUrl", void 0);
316
317
  _define_property(this, "apiKey", void 0);
317
318
  _define_property(this, "clientType", void 0);
319
+ _define_property(this, "version", void 0);
318
320
  this.baseUrl = config.baseUrl.replace(/\/$/, '');
319
321
  this.apiKey = config.apiKey;
320
322
  this.clientType = (_config_clientType = config.clientType) !== null && _config_clientType !== void 0 ? _config_clientType : 'mcp-stdio';
323
+ this.version = (_config_version = config.version) !== null && _config_version !== void 0 ? _config_version : '';
321
324
  }
322
325
  _create_class(ApiClient, [
326
+ {
327
+ key: "clientHeaders",
328
+ value: /**
329
+ * Build the request-side header set. Always carries `X-Client-Type`;
330
+ * adds `X-Client-Version` when the client was constructed with one.
331
+ */ function clientHeaders(extra) {
332
+ var headers = _object_spread({
333
+ 'X-Client-Type': this.clientType
334
+ }, this.version ? {
335
+ 'X-Client-Version': this.version
336
+ } : {}, extra);
337
+ return headers;
338
+ }
339
+ },
340
+ {
341
+ key: "observeResponse",
342
+ value: /**
343
+ * Read the registry-derived `X-Client-Latest-Version` response header
344
+ * and feed it to the update-notice comparator. Called from every
345
+ * fetch site. No-op when no version is configured (older constructions
346
+ * that didn't pass `version` should not trigger an update prompt).
347
+ */ function observeResponse(res) {
348
+ if (this.version) {
349
+ readLatestVersionFromResponse(res, this.version);
350
+ }
351
+ }
352
+ },
323
353
  {
324
354
  key: "publish",
325
355
  value: function publish(params) {
@@ -355,9 +385,7 @@ export var ApiClient = /*#__PURE__*/ function() {
355
385
  }
356
386
  return [
357
387
  2,
358
- this.doFetch('POST', '/v1/public/artifacts', form, {
359
- 'X-Client-Type': this.clientType
360
- })
388
+ this.doFetch('POST', '/v1/public/artifacts', form, this.clientHeaders())
361
389
  ];
362
390
  });
363
391
  }).call(this);
@@ -804,14 +832,14 @@ export var ApiClient = /*#__PURE__*/ function() {
804
832
  4,
805
833
  fetch(url, {
806
834
  method: 'GET',
807
- headers: {
808
- 'Authorization': "Bearer ".concat(this.apiKey),
809
- 'X-Client-Type': this.clientType
810
- }
835
+ headers: this.clientHeaders({
836
+ Authorization: "Bearer ".concat(this.apiKey)
837
+ })
811
838
  })
812
839
  ];
813
840
  case 1:
814
841
  res = _state.sent();
842
+ this.observeResponse(res);
815
843
  if (!!res.ok) return [
816
844
  3,
817
845
  3
@@ -961,10 +989,9 @@ export var ApiClient = /*#__PURE__*/ function() {
961
989
  return [
962
990
  4,
963
991
  fetch("".concat(this.baseUrl, "/v1/artifacts?limit=1"), {
964
- headers: {
965
- 'Authorization': "Bearer ".concat(apiKey),
966
- 'X-Client-Type': this.clientType
967
- }
992
+ headers: this.clientHeaders({
993
+ Authorization: "Bearer ".concat(apiKey)
994
+ })
968
995
  })
969
996
  ];
970
997
  case 1:
@@ -980,6 +1007,7 @@ export var ApiClient = /*#__PURE__*/ function() {
980
1007
  'network_error'
981
1008
  ];
982
1009
  case 3:
1010
+ this.observeResponse(res);
983
1011
  if (res.ok) {
984
1012
  return [
985
1013
  2,
@@ -1008,10 +1036,9 @@ export var ApiClient = /*#__PURE__*/ function() {
1008
1036
  return _ts_generator(this, function(_state) {
1009
1037
  return [
1010
1038
  2,
1011
- this.doFetch(method, path, body, {
1012
- 'Authorization': "Bearer ".concat(this.apiKey),
1013
- 'X-Client-Type': this.clientType
1014
- })
1039
+ this.doFetch(method, path, body, this.clientHeaders({
1040
+ Authorization: "Bearer ".concat(this.apiKey)
1041
+ }))
1015
1042
  ];
1016
1043
  });
1017
1044
  }).call(this);
@@ -1024,9 +1051,7 @@ export var ApiClient = /*#__PURE__*/ function() {
1024
1051
  return _ts_generator(this, function(_state) {
1025
1052
  return [
1026
1053
  2,
1027
- this.doFetch(method, path, body, {
1028
- 'X-Client-Type': this.clientType
1029
- })
1054
+ this.doFetch(method, path, body, this.clientHeaders())
1030
1055
  ];
1031
1056
  });
1032
1057
  }).call(this);
@@ -1045,15 +1070,15 @@ export var ApiClient = /*#__PURE__*/ function() {
1045
1070
  4,
1046
1071
  fetch(url, {
1047
1072
  method: method,
1048
- headers: {
1049
- 'Content-Type': 'application/json',
1050
- 'X-Client-Type': this.clientType
1051
- },
1073
+ headers: this.clientHeaders({
1074
+ 'Content-Type': 'application/json'
1075
+ }),
1052
1076
  body: body ? JSON.stringify(body) : undefined
1053
1077
  })
1054
1078
  ];
1055
1079
  case 1:
1056
1080
  res = _state.sent();
1081
+ this.observeResponse(res);
1057
1082
  return [
1058
1083
  2,
1059
1084
  res.json()
@@ -1088,6 +1113,7 @@ export var ApiClient = /*#__PURE__*/ function() {
1088
1113
  ];
1089
1114
  case 1:
1090
1115
  res = _state.sent();
1116
+ this.observeResponse(res);
1091
1117
  if (!!res.ok) return [
1092
1118
  3,
1093
1119
  3
package/dist/main.js CHANGED
@@ -198,9 +198,26 @@ import { Command } from 'commander';
198
198
  import { ApiClient, ApiError } from './api-client.js';
199
199
  import { loadConfig, saveConfig } from './config.js';
200
200
  import { startMcpServer } from './mcp-server.js';
201
+ import { emitOnExit, setCurrentVersion, setMcpMode } from './update-notice.js';
201
202
  import { DEFAULT_API_URL, DeviceCodeDeniedError, DeviceCodeExpiredError, DeviceCodeFailedError, InvalidFlagError, PublishArgsError, classifyBrandingError, parseShortIdAndVersion, parseShowBrandingFlag, pollDeviceToken, readApiKeyFromTty, readStreamToString, resolveAuth as resolveAuthFromEnvAndConfig, validatePublishArgs } from './main-helpers.js';
202
203
  var require = createRequire(import.meta.url);
203
204
  var version = require('../package.json').version;
205
+ // Update-notice scaffolding (spec/feat-cli-update-notification.md). The
206
+ // `'exit'` hook fires on every termination path — natural completion,
207
+ // `process.exit(N)`, uncaught throws — so the notice lands regardless of
208
+ // which exit path the command took. `'beforeExit'` would not work; it
209
+ // does not fire when `process.exit()` is called explicitly.
210
+ //
211
+ // Process-level idempotency: keyed on a Symbol.for so `vi.resetModules()`
212
+ // + dynamic re-import in tests does not stack a second listener (each
213
+ // re-import would otherwise re-run this top-level statement).
214
+ setCurrentVersion(version);
215
+ var EXIT_HOOK_FLAG = Symbol.for('@displaydev/cli.exit-hook-registered');
216
+ var flagged = process;
217
+ if (!flagged[EXIT_HOOK_FLAG]) {
218
+ flagged[EXIT_HOOK_FLAG] = true;
219
+ process.on('exit', emitOnExit);
220
+ }
204
221
  function resolveAuthOrConfig() {
205
222
  return _async_to_generator(function() {
206
223
  var auth;
@@ -260,7 +277,8 @@ function createClient(auth) {
260
277
  return new ApiClient({
261
278
  baseUrl: auth.apiUrl,
262
279
  apiKey: auth.apiKey,
263
- clientType: 'cli'
280
+ clientType: 'cli',
281
+ version: version
264
282
  });
265
283
  }
266
284
  var program = new Command().name('dsp').description('display.dev CLI — publish artifacts behind company auth').version(version);
@@ -407,7 +425,8 @@ program.command('publish <path>').description('Publish an HTML or Markdown file.
407
425
  publicClient = new ApiClient({
408
426
  baseUrl: resolvePublicApiUrl(),
409
427
  apiKey: '',
410
- clientType: 'cli'
428
+ clientType: 'cli',
429
+ version: version
411
430
  });
412
431
  _state.label = 10;
413
432
  case 10:
@@ -1074,7 +1093,8 @@ program.command('login').description('Authenticate with display.dev').option('--
1074
1093
  client = new ApiClient({
1075
1094
  baseUrl: apiUrl,
1076
1095
  apiKey: '',
1077
- clientType: 'cli'
1096
+ clientType: 'cli',
1097
+ version: version
1078
1098
  });
1079
1099
  if (!(opts.apiKey !== undefined)) return [
1080
1100
  3,
@@ -2247,6 +2267,7 @@ program.command('mcp').description('Start MCP server over stdin/stdout').action(
2247
2267
  ];
2248
2268
  case 1:
2249
2269
  auth = _state.sent();
2270
+ setMcpMode();
2250
2271
  if (!auth) return [
2251
2272
  3,
2252
2273
  3
@@ -2254,7 +2275,8 @@ program.command('mcp').description('Start MCP server over stdin/stdout').action(
2254
2275
  client = new ApiClient({
2255
2276
  baseUrl: auth.apiUrl,
2256
2277
  apiKey: auth.apiKey,
2257
- clientType: 'mcp-stdio'
2278
+ clientType: 'mcp-stdio',
2279
+ version: version
2258
2280
  });
2259
2281
  return [
2260
2282
  4,
@@ -2274,7 +2296,8 @@ program.command('mcp').description('Start MCP server over stdin/stdout').action(
2274
2296
  publicClient = new ApiClient({
2275
2297
  baseUrl: resolvePublicApiUrl(),
2276
2298
  apiKey: '',
2277
- clientType: 'mcp-stdio'
2299
+ clientType: 'mcp-stdio',
2300
+ version: version
2278
2301
  });
2279
2302
  return [
2280
2303
  4,
@@ -192,6 +192,7 @@ import { z } from 'zod';
192
192
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
193
193
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
194
194
  import { ApiError } from './api-client.js';
195
+ import { withUpdateNotice } from './update-notice.js';
195
196
  var FILE_ERROR_CODES = new Set([
196
197
  'ENOENT',
197
198
  'EACCES',
@@ -313,7 +314,7 @@ export function registerPublicTools(server, api) {
313
314
  'html',
314
315
  'md'
315
316
  ]).default('html').describe('Content format')
316
- }, function(args) {
317
+ }, withUpdateNotice(function(args) {
317
318
  return _async_to_generator(function() {
318
319
  var hasContent, hasFilePath, content, _tmp, result, err;
319
320
  return _ts_generator(this, function(_state) {
@@ -409,7 +410,7 @@ export function registerPublicTools(server, api) {
409
410
  }
410
411
  });
411
412
  })();
412
- });
413
+ }));
413
414
  }
414
415
  export function registerTools(server, api) {
415
416
  server.tool('publish', 'Publish an HTML or Markdown artifact behind company auth', {
@@ -433,7 +434,7 @@ export function registerTools(server, api) {
433
434
  'hide',
434
435
  'inherit'
435
436
  ]).optional().describe('display.dev attribution bar override. Paid tier only; defaults to org setting when omitted.')
436
- }, function(args) {
437
+ }, withUpdateNotice(function(args) {
437
438
  return _async_to_generator(function() {
438
439
  var _args_name, hasContent, hasFilePath, _args_name1, content, _tmp, result, _tmp1, err;
439
440
  return _ts_generator(this, function(_state) {
@@ -611,7 +612,7 @@ export function registerTools(server, api) {
611
612
  }
612
613
  });
613
614
  })();
614
- });
615
+ }));
615
616
  server.tool('find', 'List or search artifacts in the caller\'s organization. Omit `query` to list every artifact (paginated). Filter by name, one-or-more authors, one-or-more visibilities, or update time. Sort by `updated_at` (default), `view_count`, or `name`. Paginate via `cursor` — if the result includes a non-null `nextCursor`, pass it back to fetch the next page; if filters change between calls, drop the cursor. The response also carries `totalCount` (`-1` means the count query exceeded its budget — treat as "many"). `visibility: ["private"]` returns only your own private artifacts, not org-wide.', {
616
617
  query: z.string().optional().describe('Search by artifact name (case-insensitive substring). Omit to list all artifacts.'),
617
618
  author: z.array(z.string()).optional().describe('Filter by one or more authors. Each value: email, userId, or the literal "me". Multiple values are ORed. "me" requires a user-scoped credential — with an org API key the wire drops it.'),
@@ -632,7 +633,7 @@ export function registerTools(server, api) {
632
633
  ]).optional().describe('Sort direction. Defaults: desc for updated_at and view_count, asc for name.'),
633
634
  cursor: z.string().optional().describe('Opaque pagination token from a prior response. Pass it back to fetch the next page. WARNING: only valid for the same `sort`/`dir`/filter set that issued it — change a filter and drop the cursor, or results will be skewed without an error.'),
634
635
  limit: z.number().int().min(1).max(100).optional().describe('Max results per page (1–100, default 50).')
635
- }, function(args) {
636
+ }, withUpdateNotice(function(args) {
636
637
  return _async_to_generator(function() {
637
638
  var result, err;
638
639
  return _ts_generator(this, function(_state) {
@@ -667,11 +668,11 @@ export function registerTools(server, api) {
667
668
  }
668
669
  });
669
670
  })();
670
- });
671
+ }));
671
672
  server.tool('get', 'Get full details of a specific artifact', {
672
673
  short_id: z.string().describe('Artifact shortId'),
673
674
  include: z.array(z.string()).optional().describe('Include additional data (e.g. "versions")')
674
- }, function(args) {
675
+ }, withUpdateNotice(function(args) {
675
676
  return _async_to_generator(function() {
676
677
  var result, err;
677
678
  return _ts_generator(this, function(_state) {
@@ -706,11 +707,11 @@ export function registerTools(server, api) {
706
707
  }
707
708
  });
708
709
  })();
709
- });
710
+ }));
710
711
  server.tool('delete', 'Delete an artifact permanently', {
711
712
  short_id: z.string().describe('Artifact shortId to delete'),
712
713
  confirm: z.boolean().describe('Must be true to confirm deletion')
713
- }, function(args) {
714
+ }, withUpdateNotice(function(args) {
714
715
  return _async_to_generator(function() {
715
716
  var result, err;
716
717
  return _ts_generator(this, function(_state) {
@@ -764,7 +765,7 @@ export function registerTools(server, api) {
764
765
  }
765
766
  });
766
767
  })();
767
- });
768
+ }));
768
769
  server.tool('share', 'Change an artifact\'s visibility and/or add/remove individual shared-with emails without republishing.', {
769
770
  short_id: z.string().describe('Short ID of the artifact (8 chars).'),
770
771
  visibility: z.enum([
@@ -774,7 +775,7 @@ export function registerTools(server, api) {
774
775
  ]).optional().describe('Change the visibility level. Omit to keep current. "private" requires the Pro plan.'),
775
776
  add_users: z.array(z.string()).optional().describe('Emails to add to sharedWith (idempotent).'),
776
777
  remove_users: z.array(z.string()).optional().describe('Emails to remove from sharedWith (idempotent).')
777
- }, function(args) {
778
+ }, withUpdateNotice(function(args) {
778
779
  return _async_to_generator(function() {
779
780
  var result, err;
780
781
  return _ts_generator(this, function(_state) {
@@ -832,11 +833,11 @@ export function registerTools(server, api) {
832
833
  }
833
834
  });
834
835
  })();
835
- });
836
+ }));
836
837
  server.tool('rename', 'Rename a published artifact. The URL slug updates to match; existing URLs still resolve because routing uses the shortId.', {
837
838
  short_id: z.string().describe('The 8-character shortId returned at publish.'),
838
839
  name: z.string().describe('New display name. 1–200 characters.')
839
- }, function(args) {
840
+ }, withUpdateNotice(function(args) {
840
841
  return _async_to_generator(function() {
841
842
  var result, err;
842
843
  return _ts_generator(this, function(_state) {
@@ -875,7 +876,7 @@ export function registerTools(server, api) {
875
876
  }
876
877
  });
877
878
  })();
878
- });
879
+ }));
879
880
  server.tool('export', 'Retrieve the source bytes of a published artifact. `format: "original"` returns the publisher\'s upload (markdown for .md, HTML for .html). `format: "markdown"` always returns markdown — `.md` is unchanged, `.html` is converted via Workers AI (cached).', {
880
881
  short_id: z.string().describe('Short ID of the artifact (8 chars).'),
881
882
  version: z.number().int().min(1).optional().describe('Pinned version number. Omit for current.'),
@@ -883,7 +884,7 @@ export function registerTools(server, api) {
883
884
  'original',
884
885
  'markdown'
885
886
  ]).default('original').describe('Representation requested. Default "original".')
886
- }, function(args) {
887
+ }, withUpdateNotice(function(args) {
887
888
  return _async_to_generator(function() {
888
889
  var result, err;
889
890
  return _ts_generator(this, function(_state) {
@@ -931,7 +932,7 @@ export function registerTools(server, api) {
931
932
  }
932
933
  });
933
934
  })();
934
- });
935
+ }));
935
936
  // Metadata-only: does NOT bump the artifact version. Use the `publish`
936
937
  // tool (with `show_branding`) when you're changing content at the same
937
938
  // time so both writes happen in one transaction.
@@ -942,7 +943,7 @@ export function registerTools(server, api) {
942
943
  'hide',
943
944
  'inherit'
944
945
  ]).describe('show = force branding on; hide = force off; inherit = follow org default')
945
- }, function(args) {
946
+ }, withUpdateNotice(function(args) {
946
947
  return _async_to_generator(function() {
947
948
  var result, err;
948
949
  return _ts_generator(this, function(_state) {
@@ -977,14 +978,14 @@ export function registerTools(server, api) {
977
978
  }
978
979
  });
979
980
  })();
980
- });
981
+ }));
981
982
  server.tool('set_logo', 'Upload or replace the org logo (paid tiers only). Used as the favicon on every artifact the org publishes. Stdio variant reads bytes from a local file path.', {
982
983
  file_path: z.string().describe('Path to a local PNG or WebP file. Square, 32×32 to 1024×1024, ≤256 KB.'),
983
984
  content_type: z.enum([
984
985
  'image/png',
985
986
  'image/webp'
986
987
  ]).optional().describe('Optional declared MIME type; inferred from the file extension when omitted.')
987
- }, function(args) {
988
+ }, withUpdateNotice(function(args) {
988
989
  return _async_to_generator(function() {
989
990
  var bytes, err, contentType, lower, result, err1;
990
991
  return _ts_generator(this, function(_state) {
@@ -1073,8 +1074,8 @@ export function registerTools(server, api) {
1073
1074
  }
1074
1075
  });
1075
1076
  })();
1076
- });
1077
- server.tool('clear_logo', 'Remove the org logo. Idempotent — succeeds even when no logo is currently set.', {}, function() {
1077
+ }));
1078
+ server.tool('clear_logo', 'Remove the org logo. Idempotent — succeeds even when no logo is currently set.', {}, withUpdateNotice(function() {
1078
1079
  return _async_to_generator(function() {
1079
1080
  var err;
1080
1081
  return _ts_generator(this, function(_state) {
@@ -1111,7 +1112,7 @@ export function registerTools(server, api) {
1111
1112
  }
1112
1113
  });
1113
1114
  })();
1114
- });
1115
+ }));
1115
1116
  registerCommentTools(server, api);
1116
1117
  }
1117
1118
  /**
@@ -1179,7 +1180,7 @@ export function registerTools(server, api) {
1179
1180
  body: z.string().min(1).max(10000).describe('Comment body (≤10k chars)'),
1180
1181
  parent_id: z.string().optional().describe('Root comment id when posting a reply'),
1181
1182
  anchor: anchorSchema
1182
- }), function(args) {
1183
+ }), withUpdateNotice(function(args) {
1183
1184
  return _async_to_generator(function() {
1184
1185
  var shortIdOrErr, result, err;
1185
1186
  return _ts_generator(this, function(_state) {
@@ -1233,7 +1234,7 @@ export function registerTools(server, api) {
1233
1234
  }
1234
1235
  });
1235
1236
  })();
1236
- });
1237
+ }));
1237
1238
  server.tool('list_comments', 'List comment threads on a subject. Reads from Postgres (not the KV widget cache).', _object_spread_props(_object_spread({}, subjectArgs), {
1238
1239
  status: z.enum([
1239
1240
  'open',
@@ -1241,7 +1242,7 @@ export function registerTools(server, api) {
1241
1242
  'all'
1242
1243
  ]).optional().describe('Defaults to "open"'),
1243
1244
  since: z.string().optional().describe('ISO-8601 — return only threads with activity at or after this timestamp')
1244
- }), function(args) {
1245
+ }), withUpdateNotice(function(args) {
1245
1246
  return _async_to_generator(function() {
1246
1247
  var shortIdOrErr, result, err;
1247
1248
  return _ts_generator(this, function(_state) {
@@ -1294,11 +1295,11 @@ export function registerTools(server, api) {
1294
1295
  }
1295
1296
  });
1296
1297
  })();
1297
- });
1298
+ }));
1298
1299
  server.tool('edit_comment', 'Edit your own comment within the 5-minute author window. After 5 minutes the body is locked.', {
1299
1300
  comment_id: z.string(),
1300
1301
  body: z.string().min(1).max(10000)
1301
- }, function(args) {
1302
+ }, withUpdateNotice(function(args) {
1302
1303
  return _async_to_generator(function() {
1303
1304
  var result, err;
1304
1305
  return _ts_generator(this, function(_state) {
@@ -1333,10 +1334,10 @@ export function registerTools(server, api) {
1333
1334
  }
1334
1335
  });
1335
1336
  })();
1336
- });
1337
+ }));
1337
1338
  server.tool('delete_comment', 'Soft-delete a comment. Author / artifact creator / org admin. Replies remain visible; body becomes "[deleted]".', {
1338
1339
  comment_id: z.string()
1339
- }, function(args) {
1340
+ }, withUpdateNotice(function(args) {
1340
1341
  return _async_to_generator(function() {
1341
1342
  var err;
1342
1343
  return _ts_generator(this, function(_state) {
@@ -1373,10 +1374,10 @@ export function registerTools(server, api) {
1373
1374
  }
1374
1375
  });
1375
1376
  })();
1376
- });
1377
+ }));
1377
1378
  server.tool('resolve_thread', 'Mark a thread resolved (root comment id). Thread participant / artifact creator / org admin.', {
1378
1379
  comment_id: z.string()
1379
- }, function(args) {
1380
+ }, withUpdateNotice(function(args) {
1380
1381
  return _async_to_generator(function() {
1381
1382
  var result, err;
1382
1383
  return _ts_generator(this, function(_state) {
@@ -1411,10 +1412,10 @@ export function registerTools(server, api) {
1411
1412
  }
1412
1413
  });
1413
1414
  })();
1414
- });
1415
+ }));
1415
1416
  server.tool('reopen_thread', 'Reopen a resolved thread. Any authenticated user with view access on the artifact.', {
1416
1417
  comment_id: z.string()
1417
- }, function(args) {
1418
+ }, withUpdateNotice(function(args) {
1418
1419
  return _async_to_generator(function() {
1419
1420
  var result, err;
1420
1421
  return _ts_generator(this, function(_state) {
@@ -1449,8 +1450,8 @@ export function registerTools(server, api) {
1449
1450
  }
1450
1451
  });
1451
1452
  })();
1452
- });
1453
- server.tool('watch', 'Watch the subject for comment notifications. Per-user; service-account API keys cannot subscribe.', subjectArgs, function(args) {
1453
+ }));
1454
+ server.tool('watch', 'Watch the subject for comment notifications. Per-user; service-account API keys cannot subscribe.', subjectArgs, withUpdateNotice(function(args) {
1454
1455
  return _async_to_generator(function() {
1455
1456
  var shortIdOrErr, result, err;
1456
1457
  return _ts_generator(this, function(_state) {
@@ -1500,8 +1501,8 @@ export function registerTools(server, api) {
1500
1501
  }
1501
1502
  });
1502
1503
  })();
1503
- });
1504
- server.tool('unwatch', 'Unwatch the subject. Per-user; service-account API keys cannot subscribe.', subjectArgs, function(args) {
1504
+ }));
1505
+ server.tool('unwatch', 'Unwatch the subject. Per-user; service-account API keys cannot subscribe.', subjectArgs, withUpdateNotice(function(args) {
1505
1506
  return _async_to_generator(function() {
1506
1507
  var shortIdOrErr, err;
1507
1508
  return _ts_generator(this, function(_state) {
@@ -1553,5 +1554,5 @@ export function registerTools(server, api) {
1553
1554
  }
1554
1555
  });
1555
1556
  })();
1556
- });
1557
+ }));
1557
1558
  }
@@ -0,0 +1,488 @@
1
+ function _array_like_to_array(arr, len) {
2
+ if (len == null || len > arr.length) len = arr.length;
3
+ for(var i = 0, arr2 = new Array(len); i < len; i++)arr2[i] = arr[i];
4
+ return arr2;
5
+ }
6
+ function _array_without_holes(arr) {
7
+ if (Array.isArray(arr)) return _array_like_to_array(arr);
8
+ }
9
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) {
10
+ try {
11
+ var info = gen[key](arg);
12
+ var value = info.value;
13
+ } catch (error) {
14
+ reject(error);
15
+ return;
16
+ }
17
+ if (info.done) {
18
+ resolve(value);
19
+ } else {
20
+ Promise.resolve(value).then(_next, _throw);
21
+ }
22
+ }
23
+ function _async_to_generator(fn) {
24
+ return function() {
25
+ var self = this, args = arguments;
26
+ return new Promise(function(resolve, reject) {
27
+ var gen = fn.apply(self, args);
28
+ function _next(value) {
29
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
30
+ }
31
+ function _throw(err) {
32
+ asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
33
+ }
34
+ _next(undefined);
35
+ });
36
+ };
37
+ }
38
+ function _iterable_to_array(iter) {
39
+ if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
40
+ }
41
+ function _non_iterable_spread() {
42
+ throw new TypeError("Invalid attempt to spread non-iterable instance.\\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
43
+ }
44
+ function _to_consumable_array(arr) {
45
+ return _array_without_holes(arr) || _iterable_to_array(arr) || _unsupported_iterable_to_array(arr) || _non_iterable_spread();
46
+ }
47
+ function _type_of(obj) {
48
+ "@swc/helpers - typeof";
49
+ return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj;
50
+ }
51
+ function _unsupported_iterable_to_array(o, minLen) {
52
+ if (!o) return;
53
+ if (typeof o === "string") return _array_like_to_array(o, minLen);
54
+ var n = Object.prototype.toString.call(o).slice(8, -1);
55
+ if (n === "Object" && o.constructor) n = o.constructor.name;
56
+ if (n === "Map" || n === "Set") return Array.from(n);
57
+ if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _array_like_to_array(o, minLen);
58
+ }
59
+ function _ts_generator(thisArg, body) {
60
+ var f, y, t, _ = {
61
+ label: 0,
62
+ sent: function() {
63
+ if (t[0] & 1) throw t[1];
64
+ return t[1];
65
+ },
66
+ trys: [],
67
+ ops: []
68
+ }, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype), d = Object.defineProperty;
69
+ return d(g, "next", {
70
+ value: verb(0)
71
+ }), d(g, "throw", {
72
+ value: verb(1)
73
+ }), d(g, "return", {
74
+ value: verb(2)
75
+ }), typeof Symbol === "function" && d(g, Symbol.iterator, {
76
+ value: function() {
77
+ return this;
78
+ }
79
+ }), g;
80
+ function verb(n) {
81
+ return function(v) {
82
+ return step([
83
+ n,
84
+ v
85
+ ]);
86
+ };
87
+ }
88
+ function step(op) {
89
+ if (f) throw new TypeError("Generator is already executing.");
90
+ while(g && (g = 0, op[0] && (_ = 0)), _)try {
91
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
92
+ if (y = 0, t) op = [
93
+ op[0] & 2,
94
+ t.value
95
+ ];
96
+ switch(op[0]){
97
+ case 0:
98
+ case 1:
99
+ t = op;
100
+ break;
101
+ case 4:
102
+ _.label++;
103
+ return {
104
+ value: op[1],
105
+ done: false
106
+ };
107
+ case 5:
108
+ _.label++;
109
+ y = op[1];
110
+ op = [
111
+ 0
112
+ ];
113
+ continue;
114
+ case 7:
115
+ op = _.ops.pop();
116
+ _.trys.pop();
117
+ continue;
118
+ default:
119
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) {
120
+ _ = 0;
121
+ continue;
122
+ }
123
+ if (op[0] === 3 && (!t || op[1] > t[0] && op[1] < t[3])) {
124
+ _.label = op[1];
125
+ break;
126
+ }
127
+ if (op[0] === 6 && _.label < t[1]) {
128
+ _.label = t[1];
129
+ t = op;
130
+ break;
131
+ }
132
+ if (t && _.label < t[2]) {
133
+ _.label = t[2];
134
+ _.ops.push(op);
135
+ break;
136
+ }
137
+ if (t[2]) _.ops.pop();
138
+ _.trys.pop();
139
+ continue;
140
+ }
141
+ op = body.call(thisArg, _);
142
+ } catch (e) {
143
+ op = [
144
+ 6,
145
+ e
146
+ ];
147
+ y = 0;
148
+ } finally{
149
+ f = t = 0;
150
+ }
151
+ if (op[0] & 5) throw op[1];
152
+ return {
153
+ value: op[0] ? op[1] : void 0,
154
+ done: true
155
+ };
156
+ }
157
+ }
158
+ /**
159
+ * Update-notification surface for `@displaydev/cli`. Two paths consume it:
160
+ * plain CLI (stderr block printed from a `process.on('exit', ...)` hook)
161
+ * and MCP stdio (content block appended to the next `tools/call` result).
162
+ *
163
+ * Module-level state (`latestVersion`, `injectedThisProcess`,
164
+ * `currentVersion`) is intentional. Tests reset between cases via
165
+ * `vi.resetModules()` + dynamic re-import — there is no exported
166
+ * test-only reset.
167
+ */ import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
168
+ import { homedir } from 'node:os';
169
+ import { dirname, join } from 'node:path';
170
+ export var PACKAGE_NAME = '@displaydev/cli';
171
+ var NOTICE_PREFIX = '[displaydev]';
172
+ var THROTTLE_MS = 24 * 60 * 60 * 1000;
173
+ var latestVersion = null;
174
+ var injectedThisProcess = false;
175
+ var currentVersion = null;
176
+ var mcpMode = false;
177
+ /**
178
+ * Stamp the running CLI's version. main.ts calls this once at module
179
+ * scope; the value is read by `emitOnExit` and `injectIntoToolResult`
180
+ * when they format the notice text.
181
+ */ export function setCurrentVersion(version) {
182
+ currentVersion = version;
183
+ }
184
+ /**
185
+ * Mark this process as a long-running MCP-stdio server. main.ts calls
186
+ * this from the `mcp` subcommand handler. Once set, `emitOnExit` skips —
187
+ * MCP mode surfaces the notice via `injectIntoToolResult`, and the
188
+ * stderr fallback would (a) write the wrong text variant ("npm i -g"
189
+ * vs. "bump the host config") and (b) reach nowhere useful since stdio
190
+ * MCP hosts do not surface stderr to the agent.
191
+ */ export function setMcpMode() {
192
+ mcpMode = true;
193
+ }
194
+ /** Mark the CLI outdated, capturing the registry's `latest` value. */ export function markOutdated(version) {
195
+ latestVersion = version;
196
+ }
197
+ /** Read the most recently observed `latest`, or null. */ export function getLatestVersion() {
198
+ return latestVersion;
199
+ }
200
+ function statePath() {
201
+ return join(homedir(), '.displaydev', 'update-state.json');
202
+ }
203
+ /** Read the per-machine throttle state. Missing or corrupt → empty. */ export function loadUpdateState() {
204
+ try {
205
+ var raw = readFileSync(statePath(), 'utf-8');
206
+ var parsed = JSON.parse(raw);
207
+ if (parsed && (typeof parsed === "undefined" ? "undefined" : _type_of(parsed)) === 'object') {
208
+ return parsed;
209
+ }
210
+ return {};
211
+ } catch (unused) {
212
+ return {};
213
+ }
214
+ }
215
+ /** Persist the throttle state. Best-effort; failures swallow. */ export function saveUpdateState(state) {
216
+ try {
217
+ var path = statePath();
218
+ mkdirSync(dirname(path), {
219
+ recursive: true
220
+ });
221
+ writeFileSync(path, "".concat(JSON.stringify(state), "\n"));
222
+ } catch (unused) {
223
+ // Best-effort per spec failure-modes table — a write that fails
224
+ // means we re-fire next invocation, which is preferable to a
225
+ // crash on read-only home or a full disk.
226
+ }
227
+ }
228
+ /**
229
+ * Compare two semver strings; returns true when `a > b`. Implements the
230
+ * pre-release ranking rules from semver.org §11:
231
+ * - Numeric identifiers compare numerically.
232
+ * - Alphanumeric identifiers compare lexically (ASCII).
233
+ * - Numeric identifiers always rank below alphanumeric.
234
+ * - A longer pre-release identifier list outranks a prefix-equal shorter
235
+ * one (e.g. `0.20.0-alpha.1` > `0.20.0-alpha`).
236
+ * - A stable release outranks any pre-release with the same `x.y.z`.
237
+ *
238
+ * Returns false for anything that fails to parse.
239
+ */ export function semverGt(a, b) {
240
+ // Strict semver (semver.org §9): numeric identifiers MUST NOT have
241
+ // leading zeros, identifiers MUST NOT be empty.
242
+ var NUMERIC_RE = /^(?:0|[1-9]\d*)$/;
243
+ var ALPHANUM_RE = /^[0-9A-Za-z-]+$/;
244
+ var SEMVER_RE = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/;
245
+ var isValidPreId = function isValidPreId(id) {
246
+ if (id.length === 0) {
247
+ return false;
248
+ }
249
+ // Numeric identifier with leading zeros is invalid (`-01`, `-007`).
250
+ if (/^\d+$/.test(id) && id.length > 1 && id.startsWith('0')) {
251
+ return false;
252
+ }
253
+ return ALPHANUM_RE.test(id);
254
+ };
255
+ var parsed = function parsed(v) {
256
+ var match = SEMVER_RE.exec(v);
257
+ if (!match) {
258
+ return null;
259
+ }
260
+ // Reject leading zeros on the numeric main triple too (`01.0.0`).
261
+ if (![
262
+ match[1],
263
+ match[2],
264
+ match[3]
265
+ ].every(function(p) {
266
+ return NUMERIC_RE.test(p);
267
+ })) {
268
+ return null;
269
+ }
270
+ var preIds = match[4] ? match[4].split('.') : [];
271
+ if (!preIds.every(isValidPreId)) {
272
+ return null;
273
+ }
274
+ return {
275
+ parts: [
276
+ Number(match[1]),
277
+ Number(match[2]),
278
+ Number(match[3])
279
+ ],
280
+ preIds: preIds
281
+ };
282
+ };
283
+ var A = parsed(a);
284
+ var B = parsed(b);
285
+ if (!A || !B) {
286
+ return false;
287
+ }
288
+ for(var i = 0; i < 3; i++){
289
+ if (A.parts[i] !== B.parts[i]) {
290
+ return A.parts[i] > B.parts[i];
291
+ }
292
+ }
293
+ // Equal main parts: stable release outranks any pre-release.
294
+ if (A.preIds.length === 0 && B.preIds.length > 0) {
295
+ return true;
296
+ }
297
+ if (A.preIds.length > 0 && B.preIds.length === 0) {
298
+ return false;
299
+ }
300
+ var len = Math.min(A.preIds.length, B.preIds.length);
301
+ for(var i1 = 0; i1 < len; i1++){
302
+ var cmp = comparePreId(A.preIds[i1], B.preIds[i1]);
303
+ if (cmp !== 0) {
304
+ return cmp > 0;
305
+ }
306
+ }
307
+ // All shared identifiers equal: longer list ranks higher.
308
+ return A.preIds.length > B.preIds.length;
309
+ }
310
+ /**
311
+ * Compare two pre-release identifiers per semver §11.4. Numeric < alphanumeric;
312
+ * pure-numeric identifiers compare numerically; alphanumerics compare lexically.
313
+ * Returns positive when `a > b`, negative when `a < b`, 0 when equal.
314
+ */ function comparePreId(a, b) {
315
+ var numA = /^\d+$/.test(a);
316
+ var numB = /^\d+$/.test(b);
317
+ if (numA && numB) {
318
+ var na = Number(a);
319
+ var nb = Number(b);
320
+ return na === nb ? 0 : na > nb ? 1 : -1;
321
+ }
322
+ if (numA) {
323
+ return -1;
324
+ }
325
+ if (numB) {
326
+ return 1;
327
+ }
328
+ return a === b ? 0 : a > b ? 1 : -1;
329
+ }
330
+ /**
331
+ * Read `X-Client-Latest-Version` from a Response and, when greater than
332
+ * the local CLI version, mark the comparator outdated. Silently skips
333
+ * missing / blank / malformed values.
334
+ */ export function readLatestVersionFromResponse(res, current) {
335
+ var header = res.headers.get('x-client-latest-version');
336
+ if (!header) {
337
+ return;
338
+ }
339
+ var trimmed = header.trim();
340
+ if (!trimmed) {
341
+ return;
342
+ }
343
+ if (semverGt(trimmed, current)) {
344
+ markOutdated(trimmed);
345
+ }
346
+ }
347
+ export function formatNotice(opts) {
348
+ // Single-line dimmed banner for humans in a terminal — mode === 'cli'
349
+ // is the only path that ever sees `tty === true` (MCP is always stdio).
350
+ if (opts.mode === 'cli' && opts.tty) {
351
+ var dim = '\x1b[2m';
352
+ var reset = '\x1b[0m';
353
+ return "".concat(dim).concat(NOTICE_PREFIX, " update available: ").concat(opts.latest, " (you have ").concat(opts.current, "). Run `npm i -g ").concat(PACKAGE_NAME, "@latest` to update.").concat(reset);
354
+ }
355
+ if (opts.mode === 'cli') {
356
+ return [
357
+ "".concat(NOTICE_PREFIX, " Update available: ").concat(PACKAGE_NAME, " ").concat(opts.latest, " (you have ").concat(opts.current, ")."),
358
+ 'Ask the user if they want to update.',
359
+ "If yes: run `npm i -g ".concat(PACKAGE_NAME, "@latest` (or the equivalent for their package manager — bun, pnpm, yarn — based on how they installed it) and retry the original command.")
360
+ ].join('\n');
361
+ }
362
+ // MCP variant: npm i -g does not update an already-running stdio
363
+ // process; the host has to bump its config and restart.
364
+ return [
365
+ "".concat(NOTICE_PREFIX, " Update available: ").concat(PACKAGE_NAME, " ").concat(opts.latest, " (you have ").concat(opts.current, ")."),
366
+ 'Ask the user if they want to update.',
367
+ "If yes: bump the version pin for ".concat(PACKAGE_NAME, " in their MCP host config, or if the config uses @latest, ask them to restart their MCP host.")
368
+ ].join('\n');
369
+ }
370
+ function passesThrottle(now, state) {
371
+ if (!state.lastNoticeAt) {
372
+ return true;
373
+ }
374
+ var last = Date.parse(state.lastNoticeAt);
375
+ if (Number.isNaN(last)) {
376
+ return true;
377
+ }
378
+ return now - last >= THROTTLE_MS;
379
+ }
380
+ /**
381
+ * Synchronous `'exit'` handler for plain CLI mode. Fires after every
382
+ * termination path (natural completion, `process.exit(N)`, uncaught
383
+ * exception). Must remain synchronous — the event loop is winding
384
+ * down by the time it runs, so `await` would silently no-op.
385
+ *
386
+ * Skips when the process is running as a stdio MCP server (notice text
387
+ * differs and stderr is not surfaced to the host) or when injection has
388
+ * already fired in this process (MCP mode already prompted via the
389
+ * tool-result content block).
390
+ */ export function emitOnExit() {
391
+ if (mcpMode) {
392
+ return;
393
+ }
394
+ if (injectedThisProcess) {
395
+ return;
396
+ }
397
+ if (latestVersion === null || currentVersion === null) {
398
+ return;
399
+ }
400
+ var state = loadUpdateState();
401
+ var now = Date.now();
402
+ if (!passesThrottle(now, state)) {
403
+ return;
404
+ }
405
+ var text = formatNotice({
406
+ current: currentVersion,
407
+ latest: latestVersion,
408
+ mode: 'cli',
409
+ tty: Boolean(process.stderr.isTTY)
410
+ });
411
+ process.stderr.write("".concat(text, "\n"));
412
+ saveUpdateState({
413
+ lastNoticeAt: new Date(now).toISOString(),
414
+ lastNoticeVersion: latestVersion
415
+ });
416
+ }
417
+ /**
418
+ * Append a notice content block to a tool-call result when:
419
+ * 1. The comparator has marked the CLI outdated.
420
+ * 2. We have not already injected once in this process.
421
+ * 3. The cross-process 24 h throttle allows.
422
+ *
423
+ * Preserves `_meta`, `isError`, and any other top-level fields. Only
424
+ * `content` changes shape (a new entry appended).
425
+ */ export function injectIntoToolResult(result) {
426
+ if (latestVersion === null || currentVersion === null) {
427
+ return result;
428
+ }
429
+ if (injectedThisProcess) {
430
+ return result;
431
+ }
432
+ var now = Date.now();
433
+ var state = loadUpdateState();
434
+ if (!passesThrottle(now, state)) {
435
+ return result;
436
+ }
437
+ var text = formatNotice({
438
+ current: currentVersion,
439
+ latest: latestVersion,
440
+ mode: 'mcp',
441
+ tty: false
442
+ });
443
+ result.content = _to_consumable_array(result.content).concat([
444
+ {
445
+ type: 'text',
446
+ text: text
447
+ }
448
+ ]);
449
+ injectedThisProcess = true;
450
+ saveUpdateState({
451
+ lastNoticeAt: new Date(now).toISOString(),
452
+ lastNoticeVersion: latestVersion
453
+ });
454
+ return result;
455
+ }
456
+ /**
457
+ * Higher-order wrapper around an MCP tool handler. Awaits the inner
458
+ * handler then runs the result through `injectIntoToolResult`.
459
+ *
460
+ * Mounted in `mcp-server.ts` at every `server.tool(...)` registration
461
+ * site so injection runs uniformly regardless of which return shape
462
+ * (`okResponse` / `errorResponse` / direct literal / `_meta`-bearing)
463
+ * the inner handler returned.
464
+ */ export function withUpdateNotice(handler) {
465
+ return function() {
466
+ for(var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++){
467
+ args[_key] = arguments[_key];
468
+ }
469
+ return _async_to_generator(function() {
470
+ var result;
471
+ return _ts_generator(this, function(_state) {
472
+ switch(_state.label){
473
+ case 0:
474
+ return [
475
+ 4,
476
+ handler.apply(void 0, _to_consumable_array(args))
477
+ ];
478
+ case 1:
479
+ result = _state.sent();
480
+ return [
481
+ 2,
482
+ injectIntoToolResult(result)
483
+ ];
484
+ }
485
+ });
486
+ })();
487
+ };
488
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@displaydev/cli",
3
- "version": "0.19.0",
3
+ "version": "0.20.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "dsp": "dist/main.js"