@telepath-computer/television 0.1.47 → 0.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -33753,12 +33753,15 @@ var ServerEventMessageEvent = defineEvent();
33753
33753
  var LayoutMutationEvent = defineEvent();
33754
33754
  var LayoutScrollEvent = defineEvent();
33755
33755
  var ArtifactCreatedEvent = defineEvent();
33756
+ var ArtifactAttachedEvent = defineEvent();
33756
33757
  var ArtifactUpdatedEvent = defineEvent();
33757
33758
  var ArtifactRemovedEvent = defineEvent();
33758
33759
  var ArtifactContentChangedEvent = defineEvent();
33759
33760
  var ScreenCreatedEvent = defineEvent();
33760
33761
  var ScreenUpdatedEvent = defineEvent();
33761
33762
  var ScreenRemovedEvent = defineEvent();
33763
+ var ViewerActiveScreenChangedEvent = defineEvent();
33764
+ var ViewerFocusEvent = defineEvent();
33762
33765
 
33763
33766
  // ../shared/src/errors.ts
33764
33767
  function defineError(name) {
@@ -33775,6 +33778,7 @@ var RequestError = defineError("RequestError");
33775
33778
  var ValidationError = defineError("ValidationError");
33776
33779
  var NotFoundError = defineError("NotFoundError");
33777
33780
  var InvalidRequestError = defineError("InvalidRequestError");
33781
+ var ConflictError = defineError("ConflictError");
33778
33782
 
33779
33783
  // ../shared/src/acp-types.ts
33780
33784
  function isACPBridgeClientMessage(value) {
@@ -33950,21 +33954,36 @@ var ScreenClient = class {
33950
33954
  };
33951
33955
  var ArtifactClient = class {
33952
33956
  #http;
33953
- #screens;
33954
- constructor(http2, screens) {
33957
+ constructor(http2) {
33955
33958
  this.#http = http2;
33956
- this.#screens = screens;
33957
33959
  }
33958
- async create(input) {
33959
- const screenID = await this.#resolveScreenID(input.screenID);
33960
+ /**
33961
+ * Create an artifact, optionally attaching it to a screen in one round
33962
+ * trip. When `screenID` is supplied the response carries an `attached`
33963
+ * field with the resulting cardID.
33964
+ */
33965
+ create(input) {
33966
+ const body = {
33967
+ type: input.type,
33968
+ title: input.title
33969
+ };
33970
+ if (input.externalFilePath !== void 0) body.externalFilePath = input.externalFilePath;
33971
+ if (input.screenID !== void 0) body.screenID = input.screenID;
33972
+ return this.#http.requestJSON("POST", "/artifacts", body);
33973
+ }
33974
+ /**
33975
+ * List artifacts. With no filter, returns every artifact known to the
33976
+ * server. `screenID` filters to a single screen's members; `unplaced`
33977
+ * returns only artifacts attached to no screen.
33978
+ */
33979
+ list(input = {}) {
33980
+ const params = new URLSearchParams();
33981
+ if (input.screenID !== void 0) params.set("screenID", input.screenID);
33982
+ if (input.unplaced) params.set("unplaced", "true");
33983
+ const query = params.toString();
33960
33984
  return this.#http.requestJSON(
33961
- "POST",
33962
- `/screens/${encodeURIComponent(screenID)}/artifact`,
33963
- {
33964
- type: input.type,
33965
- title: input.title,
33966
- externalFilePath: input.externalFilePath
33967
- }
33985
+ "GET",
33986
+ query ? `/artifacts?${query}` : "/artifacts"
33968
33987
  );
33969
33988
  }
33970
33989
  get(input) {
@@ -33997,12 +34016,37 @@ var ArtifactClient = class {
33997
34016
  abandonPending(input) {
33998
34017
  return this.#http.requestVoid("POST", `/artifacts/${encodeURIComponent(input.artifactID)}/abandon`);
33999
34018
  }
34000
- remove(input) {
34019
+ /**
34020
+ * Attach an existing artifact to a screen. Idempotent — if already
34021
+ * attached, the existing card is returned and no event fires.
34022
+ */
34023
+ attach(input) {
34024
+ return this.#http.requestJSON(
34025
+ "POST",
34026
+ `/screens/${encodeURIComponent(input.screenID)}/artifacts/${encodeURIComponent(input.artifactID)}`
34027
+ );
34028
+ }
34029
+ /**
34030
+ * Detach an artifact from a single screen. Layout-only; the artifact
34031
+ * itself is never trashed by this call. Use `delete()` to remove the
34032
+ * artifact globally.
34033
+ */
34034
+ detach(input) {
34001
34035
  return this.#http.requestJSON(
34002
34036
  "DELETE",
34003
34037
  `/screens/${encodeURIComponent(input.screenID)}/artifacts/${encodeURIComponent(input.artifactID)}`
34004
34038
  );
34005
34039
  }
34040
+ /**
34041
+ * Globally delete an artifact: strip every referencing card from every
34042
+ * screen and trash/forget/discard the on-disk state.
34043
+ */
34044
+ delete(input) {
34045
+ return this.#http.requestJSON(
34046
+ "DELETE",
34047
+ `/artifacts/${encodeURIComponent(input.artifactID)}`
34048
+ );
34049
+ }
34006
34050
  getContent(input) {
34007
34051
  return this.#http.requestText("GET", `/artifacts/${encodeURIComponent(input.artifactID)}/content`);
34008
34052
  }
@@ -34013,18 +34057,27 @@ var ArtifactClient = class {
34013
34057
  input.content
34014
34058
  );
34015
34059
  }
34016
- async #resolveScreenID(screenID) {
34017
- if (screenID !== void 0) {
34018
- return screenID;
34019
- }
34020
- const { screens } = await this.#screens.list();
34021
- if (screens.length === 1) {
34022
- return screens[0].id;
34023
- }
34024
- throw this.#http.createError(
34025
- `screenID is required when multiple screens exist. Available screens: ${formatScreenChoices(screens)}`
34060
+ };
34061
+ var ViewerClient = class {
34062
+ #http;
34063
+ constructor(http2) {
34064
+ this.#http = http2;
34065
+ }
34066
+ state() {
34067
+ return this.#http.requestJSON("GET", "/viewer/state");
34068
+ }
34069
+ setActive(input) {
34070
+ return this.#http.requestJSON(
34071
+ "PUT",
34072
+ "/viewer/active-screen",
34073
+ { screenID: input.screenID }
34026
34074
  );
34027
34075
  }
34076
+ focus(input) {
34077
+ const body = { artifactID: input.artifactID };
34078
+ if (input.screenID !== void 0) body.screenID = input.screenID;
34079
+ return this.#http.requestJSON("POST", "/viewer/focus", body);
34080
+ }
34028
34081
  };
34029
34082
  var ViewClient = class {
34030
34083
  #http;
@@ -34039,12 +34092,14 @@ var ViewClient = class {
34039
34092
  var TelevisionClient = class {
34040
34093
  screens;
34041
34094
  artifacts;
34095
+ viewer;
34042
34096
  views;
34043
34097
  #http;
34044
34098
  constructor(serverURL, options = {}) {
34045
34099
  this.#http = new HttpRequester(serverURL, options);
34046
34100
  this.screens = new ScreenClient(this.#http);
34047
- this.artifacts = new ArtifactClient(this.#http, this.screens);
34101
+ this.artifacts = new ArtifactClient(this.#http);
34102
+ this.viewer = new ViewerClient(this.#http);
34048
34103
  this.views = new ViewClient(this.#http);
34049
34104
  }
34050
34105
  health() {
@@ -47984,14 +48039,24 @@ var createArtifactSchema = external_exports.object({
47984
48039
  title: external_exports.string(),
47985
48040
  externalFilePath: external_exports.string().optional()
47986
48041
  }).strict();
48042
+ var createArtifactBodySchema = createArtifactSchema.extend({
48043
+ screenID: external_exports.string().optional()
48044
+ }).strict();
47987
48045
  var patchArtifactSchema = external_exports.object({
47988
48046
  title: external_exports.string().optional()
47989
48047
  }).refine((value) => value.title !== void 0, { message: "title is required" });
48048
+ var setActiveScreenSchema = external_exports.object({ screenID: external_exports.string() }).strict();
48049
+ var focusSchema = external_exports.object({
48050
+ artifactID: external_exports.string(),
48051
+ screenID: external_exports.string().optional()
48052
+ }).strict();
48053
+ var HTTP_OK2 = 200;
47990
48054
  var HTTP_CREATED = 201;
47991
48055
  var HTTP_NO_CONTENT = 204;
47992
48056
  var HTTP_BAD_REQUEST = 400;
47993
48057
  var HTTP_FORBIDDEN = 403;
47994
48058
  var HTTP_NOT_FOUND = 404;
48059
+ var HTTP_CONFLICT = 409;
47995
48060
  var CORS_HEADERS = {
47996
48061
  "Access-Control-Allow-Origin": "*",
47997
48062
  "Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
@@ -48018,19 +48083,29 @@ function handleStoreError(res, error48, fallback) {
48018
48083
  sendError(res, HTTP_BAD_REQUEST, error48.message);
48019
48084
  return;
48020
48085
  }
48086
+ if (error48 instanceof ConflictError) {
48087
+ sendError(res, HTTP_CONFLICT, error48.message);
48088
+ return;
48089
+ }
48021
48090
  throw error48 instanceof Error ? error48 : new Error(fallback);
48022
48091
  }
48023
48092
  function registerRoutes(app, store, options) {
48093
+ const getConnectedClientCount = options.getConnectedClientCount ?? (() => 0);
48024
48094
  const auth = options.requireAuth ? [options.requireAuth] : [];
48025
48095
  app.use("/screens", applyCorsMiddleware);
48026
48096
  app.use("/artifacts", applyCorsMiddleware);
48027
48097
  app.use("/views", applyCorsMiddleware);
48098
+ app.use("/viewer", applyCorsMiddleware);
48028
48099
  app.options("/screens", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48029
48100
  app.options("/screens/:id", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48030
48101
  app.options("/screens/:id/artifact", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48031
48102
  app.options("/screens/:screenID/artifacts/:artifactID", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48103
+ app.options("/artifacts", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48032
48104
  app.options("/artifacts/:id/*", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48033
48105
  app.options("/artifacts/:id", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48106
+ app.options("/viewer/state", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48107
+ app.options("/viewer/active-screen", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48108
+ app.options("/viewer/focus", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48034
48109
  app.options("/views", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48035
48110
  app.options("/views/:id", (_req, res) => res.status(HTTP_NO_CONTENT).end());
48036
48111
  app.options("/views/:id/*", (_req, res) => res.status(HTTP_NO_CONTENT).end());
@@ -48091,6 +48166,21 @@ function registerRoutes(app, store, options) {
48091
48166
  handleStoreError(res, error48, "Failed to update screen");
48092
48167
  }
48093
48168
  });
48169
+ app.delete(
48170
+ "/screens/:screenID/artifacts/:artifactID",
48171
+ ...auth,
48172
+ (req, res) => {
48173
+ try {
48174
+ const result = store.detachArtifact({
48175
+ screenID: req.params.screenID,
48176
+ artifactID: req.params.artifactID
48177
+ });
48178
+ res.json(result);
48179
+ } catch (error48) {
48180
+ handleStoreError(res, error48, "Failed to detach artifact");
48181
+ }
48182
+ }
48183
+ );
48094
48184
  app.post("/screens/:id/artifact", ...auth, (req, res) => {
48095
48185
  const parsed = createArtifactSchema.safeParse(req.body);
48096
48186
  if (!parsed.success) {
@@ -48106,27 +48196,96 @@ function registerRoutes(app, store, options) {
48106
48196
  });
48107
48197
  res.status(HTTP_CREATED).json({
48108
48198
  artifact,
48109
- // Internal artifacts return the pending bundle path so the caller can
48110
- // write files into it before commit. External artifacts don't have a
48111
- // pending bundle.
48112
48199
  ...artifact.externalFilePath ? {} : { pendingPath: getArtifactPendingBundlePath(store.storagePath, artifact.id) }
48113
48200
  });
48114
48201
  } catch (error48) {
48115
48202
  handleStoreError(res, error48, "Failed to create artifact");
48116
48203
  }
48117
48204
  });
48118
- app.delete(
48205
+ app.get("/artifacts", ...auth, (req, res) => {
48206
+ const rawScreenID = req.query.screenID;
48207
+ const rawUnplaced = req.query.unplaced;
48208
+ const screenIDFilter = typeof rawScreenID === "string" ? rawScreenID : void 0;
48209
+ const unplacedOnly = rawUnplaced === "true" || rawUnplaced === "1";
48210
+ if (screenIDFilter !== void 0 && unplacedOnly) {
48211
+ sendError(res, HTTP_BAD_REQUEST, "screenID and unplaced are mutually exclusive");
48212
+ return;
48213
+ }
48214
+ if (screenIDFilter !== void 0) {
48215
+ const snapshot = store.getScreen(screenIDFilter);
48216
+ if (!snapshot) {
48217
+ sendError(res, HTTP_NOT_FOUND, `Screen not found: ${screenIDFilter}`);
48218
+ return;
48219
+ }
48220
+ res.json({ artifacts: snapshot.artifacts });
48221
+ return;
48222
+ }
48223
+ const all = store.listArtifacts();
48224
+ if (!unplacedOnly) {
48225
+ res.json({ artifacts: all });
48226
+ return;
48227
+ }
48228
+ const placed = /* @__PURE__ */ new Set();
48229
+ for (const screen of store.listScreens()) {
48230
+ for (const id of getScreenArtifactIDs(screen)) {
48231
+ placed.add(id);
48232
+ }
48233
+ }
48234
+ const unplaced = all.filter((artifact) => !placed.has(artifact.id));
48235
+ res.json({ artifacts: unplaced });
48236
+ });
48237
+ app.post("/artifacts", ...auth, (req, res) => {
48238
+ const parsed = createArtifactBodySchema.safeParse(req.body);
48239
+ if (!parsed.success) {
48240
+ sendError(res, HTTP_BAD_REQUEST, parsed.error.issues[0]?.message ?? "Invalid request body");
48241
+ return;
48242
+ }
48243
+ if (parsed.data.screenID !== void 0 && !store.getScreen(parsed.data.screenID)) {
48244
+ sendError(res, HTTP_NOT_FOUND, `Screen not found: ${parsed.data.screenID}`);
48245
+ return;
48246
+ }
48247
+ try {
48248
+ const artifact = store.createArtifact({
48249
+ type: parsed.data.type,
48250
+ title: parsed.data.title,
48251
+ externalFilePath: parsed.data.externalFilePath
48252
+ });
48253
+ const body = { artifact };
48254
+ if (!artifact.externalFilePath) {
48255
+ body.pendingPath = getArtifactPendingBundlePath(store.storagePath, artifact.id);
48256
+ }
48257
+ if (parsed.data.screenID !== void 0) {
48258
+ const attachResult = store.attachArtifact({
48259
+ screenID: parsed.data.screenID,
48260
+ artifactID: artifact.id
48261
+ });
48262
+ body.attached = { screenID: attachResult.screenID, cardID: attachResult.cardID };
48263
+ }
48264
+ res.status(HTTP_CREATED).json(body);
48265
+ } catch (error48) {
48266
+ handleStoreError(res, error48, "Failed to create artifact");
48267
+ }
48268
+ });
48269
+ app.delete("/artifacts/:id", ...auth, (req, res) => {
48270
+ try {
48271
+ const result = store.deleteArtifact(req.params.id);
48272
+ res.json(result);
48273
+ } catch (error48) {
48274
+ handleStoreError(res, error48, "Failed to delete artifact");
48275
+ }
48276
+ });
48277
+ app.post(
48119
48278
  "/screens/:screenID/artifacts/:artifactID",
48120
48279
  ...auth,
48121
48280
  (req, res) => {
48122
48281
  try {
48123
- const result = store.removeArtifactFromScreen(
48124
- req.params.artifactID,
48125
- req.params.screenID
48126
- );
48282
+ const result = store.attachArtifact({
48283
+ screenID: req.params.screenID,
48284
+ artifactID: req.params.artifactID
48285
+ });
48127
48286
  res.json(result);
48128
48287
  } catch (error48) {
48129
- handleStoreError(res, error48, "Failed to remove artifact");
48288
+ handleStoreError(res, error48, "Failed to attach artifact");
48130
48289
  }
48131
48290
  }
48132
48291
  );
@@ -48238,6 +48397,41 @@ function registerRoutes(app, store, options) {
48238
48397
  app.put("/artifacts/:id/content/*", ...auth, (_req, res) => {
48239
48398
  sendError(res, HTTP_FORBIDDEN, "Only the primary content file is HTTP-writable");
48240
48399
  });
48400
+ app.get("/viewer/state", ...auth, (_req, res) => {
48401
+ res.json({
48402
+ activeScreenID: store.getActiveScreenID(),
48403
+ connectedClients: getConnectedClientCount()
48404
+ });
48405
+ });
48406
+ app.put("/viewer/active-screen", ...auth, (req, res) => {
48407
+ const parsed = setActiveScreenSchema.safeParse(req.body);
48408
+ if (!parsed.success) {
48409
+ sendError(res, HTTP_BAD_REQUEST, parsed.error.issues[0]?.message ?? "Invalid request body");
48410
+ return;
48411
+ }
48412
+ try {
48413
+ store.setActiveScreen(parsed.data.screenID);
48414
+ res.status(HTTP_OK2).json({ activeScreenID: parsed.data.screenID });
48415
+ } catch (error48) {
48416
+ handleStoreError(res, error48, "Failed to set active screen");
48417
+ }
48418
+ });
48419
+ app.post("/viewer/focus", ...auth, (req, res) => {
48420
+ const parsed = focusSchema.safeParse(req.body);
48421
+ if (!parsed.success) {
48422
+ sendError(res, HTTP_BAD_REQUEST, parsed.error.issues[0]?.message ?? "Invalid request body");
48423
+ return;
48424
+ }
48425
+ try {
48426
+ const result = store.focus({
48427
+ artifactID: parsed.data.artifactID,
48428
+ ...parsed.data.screenID !== void 0 ? { screenID: parsed.data.screenID } : {}
48429
+ });
48430
+ res.status(HTTP_OK2).json(result);
48431
+ } catch (error48) {
48432
+ handleStoreError(res, error48, "Failed to focus artifact");
48433
+ }
48434
+ });
48241
48435
  const viewStaticHandlers = /* @__PURE__ */ new Map();
48242
48436
  const getViewStaticHandler = (id, viewDir) => {
48243
48437
  let handler = viewStaticHandlers.get(id);
@@ -48368,6 +48562,16 @@ var EventStreamServer = class extends withDisposable(class {
48368
48562
  });
48369
48563
  this.subscribeStore();
48370
48564
  }
48565
+ /** Number of currently-open `/events` sockets. */
48566
+ getConnectedClientCount() {
48567
+ let count = 0;
48568
+ for (const socket of this.wsServer.clients) {
48569
+ if (socket.readyState === import_websocket.default.OPEN) {
48570
+ count++;
48571
+ }
48572
+ }
48573
+ return count;
48574
+ }
48371
48575
  handleUpgrade(request, socket, head) {
48372
48576
  this.wsServer.handleUpgrade(request, socket, head, (ws) => {
48373
48577
  this.wsServer.emit("connection", ws, request);
@@ -48390,7 +48594,10 @@ var EventStreamServer = class extends withDisposable(class {
48390
48594
  }
48391
48595
  subscribeStore() {
48392
48596
  const onArtifactCreated = (event) => {
48393
- this.broadcast({ type: "artifact-created", screenID: event.screenID, artifact: event.artifact });
48597
+ this.broadcast({ type: "artifact-created", artifact: event.artifact });
48598
+ };
48599
+ const onArtifactAttached = (event) => {
48600
+ this.broadcast({ type: "artifact-attached", screenID: event.screenID, cardID: event.cardID, artifact: event.artifact });
48394
48601
  };
48395
48602
  const onArtifactUpdated = (event) => {
48396
48603
  this.broadcast({ type: "artifact-updated", artifact: event.artifact });
@@ -48410,21 +48617,33 @@ var EventStreamServer = class extends withDisposable(class {
48410
48617
  const onScreenRemoved = (event) => {
48411
48618
  this.broadcast({ type: "screen-removed", screenID: event.screenID });
48412
48619
  };
48620
+ const onViewerActiveScreenChanged = (event) => {
48621
+ this.broadcast({ type: "viewer-active-screen-changed", screenID: event.screenID });
48622
+ };
48623
+ const onViewerFocus = (event) => {
48624
+ this.broadcast({ type: "viewer-focus", screenID: event.screenID, artifactID: event.artifactID });
48625
+ };
48413
48626
  this.store.addEventListener("artifact-created", onArtifactCreated);
48627
+ this.store.addEventListener("artifact-attached", onArtifactAttached);
48414
48628
  this.store.addEventListener("artifact-updated", onArtifactUpdated);
48415
48629
  this.store.addEventListener("artifact-removed", onArtifactRemoved);
48416
48630
  this.store.addEventListener("artifact-content-changed", onArtifactContentChanged);
48417
48631
  this.store.addEventListener("screen-created", onScreenCreated);
48418
48632
  this.store.addEventListener("screen-updated", onScreenUpdated);
48419
48633
  this.store.addEventListener("screen-removed", onScreenRemoved);
48634
+ this.store.addEventListener("viewer-active-screen-changed", onViewerActiveScreenChanged);
48635
+ this.store.addEventListener("viewer-focus", onViewerFocus);
48420
48636
  this.unsubscribe.push(
48421
48637
  () => this.store.removeEventListener("artifact-created", onArtifactCreated),
48638
+ () => this.store.removeEventListener("artifact-attached", onArtifactAttached),
48422
48639
  () => this.store.removeEventListener("artifact-updated", onArtifactUpdated),
48423
48640
  () => this.store.removeEventListener("artifact-removed", onArtifactRemoved),
48424
48641
  () => this.store.removeEventListener("artifact-content-changed", onArtifactContentChanged),
48425
48642
  () => this.store.removeEventListener("screen-created", onScreenCreated),
48426
48643
  () => this.store.removeEventListener("screen-updated", onScreenUpdated),
48427
- () => this.store.removeEventListener("screen-removed", onScreenRemoved)
48644
+ () => this.store.removeEventListener("screen-removed", onScreenRemoved),
48645
+ () => this.store.removeEventListener("viewer-active-screen-changed", onViewerActiveScreenChanged),
48646
+ () => this.store.removeEventListener("viewer-focus", onViewerFocus)
48428
48647
  );
48429
48648
  }
48430
48649
  broadcast(event) {
@@ -48707,13 +48926,14 @@ var Server = class {
48707
48926
  res.json({ status: "ok" });
48708
48927
  });
48709
48928
  this.app.use(import_express2.default.json());
48929
+ this.events = new EventStreamServer({ store: this.store, publicServer: this.publicServer });
48710
48930
  registerRoutes(this.app, this.store, {
48711
- requireAuth: this.publicServer ? null : this.requireAuthorization
48931
+ requireAuth: this.publicServer ? null : this.requireAuthorization,
48932
+ getConnectedClientCount: () => this.events.getConnectedClientCount()
48712
48933
  });
48713
48934
  if (options.staticDir) {
48714
48935
  this.app.use(import_express2.default.static(options.staticDir));
48715
48936
  }
48716
- this.events = new EventStreamServer({ store: this.store, publicServer: this.publicServer });
48717
48937
  this.acp = new ACPServer({ authToken: this.store.authToken, publicServer: this.publicServer });
48718
48938
  this.httpServer.on("upgrade", (request, socket, head) => {
48719
48939
  const parsed = new URL(request.url ?? "/", this.baseURL);
@@ -48805,6 +49025,7 @@ var ServerStore = class extends EventTarget {
48805
49025
  storagePath;
48806
49026
  authToken;
48807
49027
  screens = /* @__PURE__ */ new Map();
49028
+ activeScreenID = null;
48808
49029
  _artifacts = /* @__PURE__ */ new Map();
48809
49030
  contentWatchers = /* @__PURE__ */ new Map();
48810
49031
  contentWatchDebounceTimers = /* @__PURE__ */ new Map();
@@ -48820,6 +49041,7 @@ var ServerStore = class extends EventTarget {
48820
49041
  this.views = this.scanViewRegistry();
48821
49042
  this.authToken = this.loadOrCreateAuthToken();
48822
49043
  this.load();
49044
+ this.loadViewerState();
48823
49045
  this.startContentWatchersForLoadedArtifacts();
48824
49046
  if (this.screens.size === 0) {
48825
49047
  this.createDefaultScreen();
@@ -48940,8 +49162,15 @@ var ServerStore = class extends EventTarget {
48940
49162
  listArtifacts() {
48941
49163
  return [...this._artifacts.values()];
48942
49164
  }
49165
+ /**
49166
+ * Create a new artifact. When `screenID` is supplied, the artifact is also
49167
+ * attached to that screen as a convenience: a card is appended to the strip
49168
+ * end and an `artifact-attached` event follows the `artifact-created` event.
49169
+ * When `screenID` is omitted, the artifact is created standalone — no screen
49170
+ * mutation, no `artifact-attached` event.
49171
+ */
48943
49172
  createArtifact(input) {
48944
- const screen = this.requireScreen(input.screenID);
49173
+ const screen = input.screenID !== void 0 ? this.requireScreen(input.screenID) : null;
48945
49174
  const artifact = createArtifact({
48946
49175
  type: input.type,
48947
49176
  title: input.title,
@@ -48956,14 +49185,19 @@ var ServerStore = class extends EventTarget {
48956
49185
  if (artifact.status === "committed") {
48957
49186
  this.startWatchingArtifactContent(artifact.id);
48958
49187
  }
48959
- screen.layout = [...screen.layout, createCardNode(artifact.id)];
48960
- this.persistScreen(screen);
48961
- this.dispatchEvent(
48962
- new ArtifactCreatedEvent("artifact-created", {
48963
- screenID: screen.id,
48964
- artifact
48965
- })
48966
- );
49188
+ this.dispatchEvent(new ArtifactCreatedEvent("artifact-created", { artifact }));
49189
+ if (screen) {
49190
+ const card = createCardNode(artifact.id);
49191
+ screen.layout = [...screen.layout, card];
49192
+ this.persistScreen(screen);
49193
+ this.dispatchEvent(
49194
+ new ArtifactAttachedEvent("artifact-attached", {
49195
+ screenID: screen.id,
49196
+ cardID: card.id,
49197
+ artifact
49198
+ })
49199
+ );
49200
+ }
48967
49201
  return artifact;
48968
49202
  }
48969
49203
  updateArtifact(input) {
@@ -49049,18 +49283,93 @@ var ServerStore = class extends EventTarget {
49049
49283
  this.dispatchEvent(new ArtifactUpdatedEvent("artifact-updated", { artifact }));
49050
49284
  }
49051
49285
  /**
49052
- * Remove an artifact reference from a single screen.
49286
+ * Attach an existing artifact to a screen by appending a default-sized
49287
+ * card to the right end of the strip. Idempotent: if the artifact is
49288
+ * already on the screen, returns the existing card and emits no event.
49053
49289
  *
49054
- * - If other screens still reference the artifact, only the screen
49055
- * layout is stripped; no disk content is touched. Result: `unlinked`.
49056
- * - If this was the last reference, the artifact cascades through the
49057
- * same trash/discard rules as before (committed → trashed, pending-create
49058
- * → discarded, external → metadata-only trashed). Result mirrors the
49059
- * committed/external/pending-create variant.
49290
+ * Throws `NotFoundError` when the screen or artifact does not exist.
49291
+ */
49292
+ attachArtifact(input) {
49293
+ const screen = this.requireScreen(input.screenID);
49294
+ const artifact = this._artifacts.get(input.artifactID);
49295
+ if (!artifact) {
49296
+ throw new NotFoundError(`Artifact not found: ${input.artifactID}`, {
49297
+ entityType: "artifact",
49298
+ entityID: input.artifactID
49299
+ });
49300
+ }
49301
+ const existingCardID = findCardIDForArtifact(screen.layout, input.artifactID);
49302
+ if (existingCardID !== null) {
49303
+ return { screenID: screen.id, cardID: existingCardID, artifact };
49304
+ }
49305
+ const card = createCardNode(input.artifactID);
49306
+ screen.layout = [...screen.layout, card];
49307
+ this.persistScreen(screen);
49308
+ this.dispatchEvent(
49309
+ new ArtifactAttachedEvent("artifact-attached", {
49310
+ screenID: screen.id,
49311
+ cardID: card.id,
49312
+ artifact
49313
+ })
49314
+ );
49315
+ return { screenID: screen.id, cardID: card.id, artifact };
49316
+ }
49317
+ /**
49318
+ * Detach an artifact reference from a single screen. Layout-only — the
49319
+ * artifact metadata, bundle, and pending state are never touched, even
49320
+ * when this is the last screen referencing it. Use `deleteArtifact` to
49321
+ * actually remove an artifact.
49060
49322
  *
49061
49323
  * Throws `NotFoundError` when the screen does not exist, the artifact
49062
49324
  * does not exist, or the artifact is not a member of the screen.
49063
49325
  */
49326
+ detachArtifact(input) {
49327
+ const screen = this.requireScreen(input.screenID);
49328
+ const artifact = this._artifacts.get(input.artifactID);
49329
+ if (!artifact) {
49330
+ throw new NotFoundError(`Artifact not found: ${input.artifactID}`, {
49331
+ entityType: "artifact",
49332
+ entityID: input.artifactID
49333
+ });
49334
+ }
49335
+ if (!layoutContainsArtifactID(screen.layout, input.artifactID)) {
49336
+ throw new NotFoundError(
49337
+ `Artifact ${input.artifactID} is not a member of screen ${input.screenID}`,
49338
+ { entityType: "artifact", entityID: input.artifactID }
49339
+ );
49340
+ }
49341
+ screen.layout = removeArtifactFromLayout(screen.layout, input.artifactID);
49342
+ this.persistScreen(screen);
49343
+ this.dispatchEvent(
49344
+ new ArtifactRemovedEvent("artifact-removed", {
49345
+ artifactID: input.artifactID,
49346
+ screenID: input.screenID
49347
+ })
49348
+ );
49349
+ return { screenID: input.screenID, artifactID: input.artifactID };
49350
+ }
49351
+ /**
49352
+ * Globally remove an artifact: strip every referencing card from every
49353
+ * screen (firing one `artifact-removed` per affected screen), then trash
49354
+ * the bundle (committed internal), forget the pointer (external), or
49355
+ * discard live state (pending-create internal).
49356
+ *
49357
+ * Throws `NotFoundError` when the artifact does not exist.
49358
+ */
49359
+ deleteArtifact(artifactID) {
49360
+ const artifact = this._artifacts.get(artifactID);
49361
+ if (!artifact) {
49362
+ throw new NotFoundError(`Artifact not found: ${artifactID}`, { entityType: "artifact", entityID: artifactID });
49363
+ }
49364
+ return this.cascadeArtifactRemoval(artifactID);
49365
+ }
49366
+ /**
49367
+ * Internal helper used by `removeScreen` to cascade per-artifact
49368
+ * cleanup with the same legacy-shaped result so existing screen-removal
49369
+ * tests continue to pin the user-visible "delete a screen, lose its
49370
+ * orphaned artifacts" behavior. New callers should use `detachArtifact`
49371
+ * or `deleteArtifact` directly.
49372
+ */
49064
49373
  removeArtifactFromScreen(artifactID, screenID) {
49065
49374
  const screen = this.requireScreen(screenID);
49066
49375
  const artifact = this._artifacts.get(artifactID);
@@ -49075,11 +49384,7 @@ var ServerStore = class extends EventTarget {
49075
49384
  }
49076
49385
  const referencingScreenIDs = this.collectReferencingScreenIDs(artifactID);
49077
49386
  if (referencingScreenIDs.some((id) => id !== screenID)) {
49078
- screen.layout = removeArtifactFromLayout(screen.layout, artifactID);
49079
- this.persistScreen(screen);
49080
- this.dispatchEvent(
49081
- new ArtifactRemovedEvent("artifact-removed", { artifactID, screenID })
49082
- );
49387
+ this.detachArtifact({ screenID, artifactID });
49083
49388
  return { outcome: "unlinked", artifactID };
49084
49389
  }
49085
49390
  return this.cascadeArtifactRemoval(artifactID);
@@ -49103,6 +49408,79 @@ var ServerStore = class extends EventTarget {
49103
49408
  this.dispatchEvent(new ScreenUpdatedEvent("screen-updated", { screen }));
49104
49409
  return screen;
49105
49410
  }
49411
+ /**
49412
+ * The screen that Television's GUI is currently focused on, or `null` if
49413
+ * no client has set one. Drift-tolerant: if the persisted ID points at a
49414
+ * screen that no longer exists, returns `null` rather than the stale ID.
49415
+ */
49416
+ getActiveScreenID() {
49417
+ if (this.activeScreenID && !this.screens.has(this.activeScreenID)) {
49418
+ return null;
49419
+ }
49420
+ return this.activeScreenID;
49421
+ }
49422
+ setActiveScreen(screenID) {
49423
+ this.requireScreen(screenID);
49424
+ if (this.activeScreenID === screenID) {
49425
+ return;
49426
+ }
49427
+ this.activeScreenID = screenID;
49428
+ this.persistViewerState();
49429
+ this.dispatchEvent(
49430
+ new ViewerActiveScreenChangedEvent("viewer-active-screen-changed", { screenID })
49431
+ );
49432
+ }
49433
+ /**
49434
+ * Resolve a target screen for `artifactID` (preferring an explicit
49435
+ * `screenID`, then the current active screen, then any screen referencing
49436
+ * the artifact), flip the active screen if necessary, then broadcast a
49437
+ * `viewer-focus` event for clients to scroll-and-highlight.
49438
+ *
49439
+ * Throws `NotFoundError` for missing artifact or unknown explicit screen,
49440
+ * `InvalidRequestError` if the artifact is not attached to an explicitly
49441
+ * named screen, and `ConflictError` if no `screenID` was supplied and the
49442
+ * artifact is attached to no screens.
49443
+ */
49444
+ focus(input) {
49445
+ const artifact = this._artifacts.get(input.artifactID);
49446
+ if (!artifact) {
49447
+ throw new NotFoundError(`Artifact not found: ${input.artifactID}`, {
49448
+ entityType: "artifact",
49449
+ entityID: input.artifactID
49450
+ });
49451
+ }
49452
+ let target;
49453
+ if (input.screenID !== void 0) {
49454
+ const screen = this.requireScreen(input.screenID);
49455
+ if (!layoutContainsArtifactID(screen.layout, input.artifactID)) {
49456
+ throw new InvalidRequestError(
49457
+ `Artifact ${input.artifactID} is not attached to screen ${input.screenID}`
49458
+ );
49459
+ }
49460
+ target = screen.id;
49461
+ } else {
49462
+ const active = this.getActiveScreenID();
49463
+ const activeScreen = active ? this.screens.get(active) : void 0;
49464
+ if (activeScreen && layoutContainsArtifactID(activeScreen.layout, input.artifactID)) {
49465
+ target = activeScreen.id;
49466
+ } else {
49467
+ const referencing = this.collectReferencingScreenIDs(input.artifactID);
49468
+ if (referencing.length === 0) {
49469
+ throw new ConflictError(
49470
+ `Artifact ${input.artifactID} is not attached to any screen; cannot focus`
49471
+ );
49472
+ }
49473
+ target = referencing[0];
49474
+ }
49475
+ }
49476
+ if (this.activeScreenID !== target) {
49477
+ this.setActiveScreen(target);
49478
+ }
49479
+ this.dispatchEvent(
49480
+ new ViewerFocusEvent("viewer-focus", { screenID: target, artifactID: input.artifactID })
49481
+ );
49482
+ return { screenID: target, artifactID: input.artifactID };
49483
+ }
49106
49484
  removeScreen(screenID) {
49107
49485
  const screen = this.requireScreen(screenID);
49108
49486
  const snapshot = { ...screen, layout: structuredClone(screen.layout) };
@@ -49387,6 +49765,35 @@ var ServerStore = class extends EventTarget {
49387
49765
  registry2.sort((a, b) => a.id.localeCompare(b.id));
49388
49766
  return registry2;
49389
49767
  }
49768
+ loadViewerState() {
49769
+ if (!(0, import_node_fs3.existsSync)(this.viewerStatePath)) {
49770
+ return;
49771
+ }
49772
+ let raw;
49773
+ try {
49774
+ raw = (0, import_node_fs3.readFileSync)(this.viewerStatePath, "utf8");
49775
+ } catch {
49776
+ return;
49777
+ }
49778
+ let parsed;
49779
+ try {
49780
+ parsed = JSON.parse(raw);
49781
+ } catch {
49782
+ return;
49783
+ }
49784
+ if (parsed && typeof parsed === "object" && "activeScreenID" in parsed && (typeof parsed.activeScreenID === "string" || parsed.activeScreenID === null)) {
49785
+ this.activeScreenID = parsed.activeScreenID;
49786
+ }
49787
+ }
49788
+ persistViewerState() {
49789
+ (0, import_node_fs3.writeFileSync)(
49790
+ this.viewerStatePath,
49791
+ JSON.stringify({ activeScreenID: this.activeScreenID }, null, JSON_INDENT_SPACES)
49792
+ );
49793
+ }
49794
+ get viewerStatePath() {
49795
+ return import_node_path6.default.join(this.storagePath, "viewer.json");
49796
+ }
49390
49797
  loadOrCreateAuthToken() {
49391
49798
  if ((0, import_node_fs3.existsSync)(this.tokenPath)) {
49392
49799
  return (0, import_node_fs3.readFileSync)(this.tokenPath, "utf8").trim();
@@ -49551,6 +49958,30 @@ Review docs/storage.md for the current schema, and either update the file to mat
49551
49958
  return import_node_path6.default.join(this.storagePath, "token");
49552
49959
  }
49553
49960
  };
49961
+ function findCardIDForArtifact(layout, artifactID) {
49962
+ for (const node of layout) {
49963
+ const id = findCardIDInNode(node, artifactID);
49964
+ if (id !== null) return id;
49965
+ }
49966
+ return null;
49967
+ }
49968
+ function findCardIDInNode(node, artifactID) {
49969
+ switch (node.type) {
49970
+ case "card":
49971
+ return node.artifactID === artifactID ? node.id : null;
49972
+ case "row":
49973
+ for (const child of node.children) {
49974
+ if (child.artifactID === artifactID) return child.id;
49975
+ }
49976
+ return null;
49977
+ case "stack":
49978
+ for (const child of node.children) {
49979
+ const id = findCardIDInNode(child, artifactID);
49980
+ if (id !== null) return id;
49981
+ }
49982
+ return null;
49983
+ }
49984
+ }
49554
49985
 
49555
49986
  // src/index.ts
49556
49987
  var import_meta = {};
@@ -49579,8 +50010,8 @@ function getDevPackageDir() {
49579
50010
  return import_node_path7.default.resolve(import_node_path7.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)), "..");
49580
50011
  }
49581
50012
  function readCLIVersion() {
49582
- if ("0.1.47".length > 0) {
49583
- return "0.1.47";
50013
+ if ("0.1.48".length > 0) {
50014
+ return "0.1.48";
49584
50015
  }
49585
50016
  const devPackageJsonPath = import_node_path7.default.join(getDevPackageDir(), "package.json");
49586
50017
  if (!(0, import_node_fs4.existsSync)(devPackageJsonPath)) {
@@ -49589,8 +50020,8 @@ function readCLIVersion() {
49589
50020
  return JSON.parse((0, import_node_fs4.readFileSync)(devPackageJsonPath, "utf8")).version;
49590
50021
  }
49591
50022
  function readWorkflowHelpText() {
49592
- if ('# Television\n\nTelevision is a persistent artifact screen for agents. Use it when the user\nshould be able to inspect, revisit, and refine a file-backed result instead of\nonly reading a chat reply.\n\nIf you lose context, run:\n\n```bash\ntv help\n```\n\nThat command prints this full skill as one blob. There is no topic-scoped help\nin the current implementation.\n\n## Mental model\n\n- A **screen** is the screen and layout container.\n- An **artifact** is one displayed result on that screen.\n- An **internal artifact** is a Television-managed bundle. You create a pending\n bundle, edit files in that bundle, then commit it.\n- An **external artifact** is a pointer to an existing absolute file on disk.\n Television displays that file but does not own or delete it.\n- **Pending** means a create or edit is staged but not yet committed.\n- **Trash** means metadata and committed internal bundles moved out of the live\n tree. There is no restore workflow in the current scope.\n\nThe core workflow is:\n\n1. Decide whether the result should be internal or external.\n2. Create or stage the artifact with the CLI.\n3. For internal artifacts, edit files in the pending bundle.\n4. Commit when the validation rules are satisfied.\n\n## User communication during multi-step workflows\n\nWhen you are doing a multi-step artifact workflow, keep the user informed as you\nprogress.\n\nRequired communication style:\n\n- verbalize key actions and decisions as they happen\n- keep the language concise\n- prefer short updates over long explanations\n- frame updates in the user\'s world and goals, not in the internal mechanics of the skill or CLI workflow\n- avoid technical workflow jargon unless the user explicitly asks for it\n- do not write reports, long paragraphs, or chatty summaries while the work is in progress\n- do not use lists unless the user explicitly asks for one\n- optimize for speed and token efficiency\n\nGood examples:\n\n- "Starting the artifact now."\n- "Reviewing the draft and source material."\n- "Updating the HTML and efficiently navigating the artifact creation flow."\n- "The artifact did not pass validation yet; fixing the draft notes and retrying."\n- "Finalizing the artifact now."\n- "Done."\n\nBad examples:\n\n- multi-paragraph progress reports\n- long retrospective narration during execution\n- verbose bullet lists for routine workflow steps\n\n## Internal versus external\n\nUse an **internal artifact** when:\n\n- the artifact is purpose-built for Television\n- Television should own the bundle structure\n- future agents should be able to maintain the result by reading bundle files\n- you need a staged create or staged edit workflow\n\nUse an **external artifact** when:\n\n- a real file already exists on disk\n- the user wants Television to display that existing file\n- you do not need a Television-managed bundle\n\nDecision rule:\n\n- If the result should be maintained as a Television-owned long-lived artifact,\n choose internal.\n- If the result is already a real file outside Television and should stay that\n way, choose external.\n\nSupported artifact types:\n\n- `text/markdown`\n- `text/html`\n\n## Internal bundle files\n\nEvery internal artifact bundle contains:\n\n- `artifact.md`\n- `data.json`\n- `memory.md`\n- `public/index.md` or `public/index.html`\n\nFresh pending bundles are intentionally minimal:\n\n- `artifact.md` is blank\n- `memory.md` is blank\n- `public/index.md` or `public/index.html` is blank\n- `data.json` is exactly `{}`\n\nThe scaffold is not commit-valid by itself. Learn the required structure from\nthis skill, not from placeholder content in the scaffold.\n\n### `artifact.md`\n\n`artifact.md` is the contract for the artifact. It explains what the artifact\nis for, what conceptual material it is based on, how it should render, and how\nlater agents should maintain it.\n\nBefore commit, `artifact.md` must be non-empty and contain all of these exact\nheadings:\n\n```md\n## User intent\n## Purpose\n## Data shape\n## Data sources\n## Rendering\n## Update workflow\n## Non-goals\n```\n\nWhat each section should capture:\n\n- `## User intent`: faithful restatement or quotation of what the user actually said they wanted; this is critical and should preserve the user\'s language as closely as practical, including requests, feedback, complaints, constraints, and guidance\n- `## Purpose`: what the artifact is trying to achieve\n- `## Data shape`: the conceptual shape you reasoned about while authoring; for markdown artifacts this will often just be `{}`\n- `## Data sources`: where the underlying facts, notes, or source material came from and how they were obtained\n- `## Rendering`: how `public/index.md` or `public/index.html` should present it\n- `## Update workflow`: how future agents should refresh or modify it\n- `## Non-goals`: what is intentionally excluded, especially application-like runtime behavior\n\n### `data.json`\n\n`data.json` is a **thinking artifact**, not a runtime payload.\n\nIts purpose is to help the model separate:\n\n- reasoning / planning / authoring structure\n- from final presentation in `public/index.md` or `public/index.html`\n\nUse it to capture the pure conceptual shape of what you are about to render.\nThis is an authoring aid for agents, not an application data layer.\n\nHard rules:\n\n- **Do not treat `data.json` as live runtime state.**\n- **Do not write HTML/JS that loads, depends on, or synchronizes against `data.json`.**\n- **Do not build application-like data-driven artifacts.**\n- **We do not support runtime data-backed artifacts at this time.**\n- Artifacts are static markdown or static HTML documents.\n- HTML artifacts may include JavaScript and extra assets under `public/`, but\n that JavaScript must stay presentation-oriented and self-contained, not\n driven by `data.json` as an application state container.\n\nFor `text/markdown` artifacts, leave `data.json` as exactly:\n\n```json\n{}\n```\n\nThere is usually little value in separating content from presentation for\nmarkdown artifacts, so prefer `{}` unless there is a very strong authoring\nreason not to.\n\nFor `text/html` artifacts, use `data.json` only when it helps you think clearly\nabout the material before rendering. It may describe the conceptual structure\nof the artifact, but it must not become a runtime contract.\n\nValidation rule:\n\n- `data.json` must exist and contain valid JSON\n\nThe current validator does not require the JSON value to be an object, but an\nobject is the normal choice.\n\n### `memory.md`\n\n`memory.md` is the working scratchpad for later agents. Record decisions,\nlimitations, data-retrieval notes, problems encountered, what changed, and what\nshould be watched during future edits.\n\nRequired validation anchors:\n\n- `memory.md` must contain `## Activity Log`\n- `memory.md` must contain at least one UTC timestamp in exact\n `YYYY-MM-DDTHH:MM:SSZ` format\n- at least one timestamp must be from the last 30 minutes when you commit\n\nThe minimum required heading is:\n\n```md\n## Activity Log\n```\n\nWhat to record beyond that is up to the artifact and the work performed.\n\n### `public/index.md` and `public/index.html`\n\nThis is the rendered entry file that Television serves.\n\n- Markdown artifacts use `public/index.md`\n- HTML artifacts use `public/index.html`\n- the entry file must match the artifact `type`\n- the entry file must be non-empty before commit\n\nFor HTML artifacts:\n\n- `public/index.html` is a full HTML document, not a body fragment\n- additional public assets may live under `public/`\n- keep paths relative to `public/`\n\n## Quality bar\n\nBuild artifacts that are durable, truthful, and maintainable by later agents.\n\nRequired quality standards:\n\n- be faithful to source data\n- do not invent or hallucinate missing facts\n- do not silently truncate a dataset and pretend it is complete\n- prefer truth over completeness when those goals conflict\n- make limitations, sampling, missing data, and freshness visible\n- keep rendering aligned with the reasoning captured in `artifact.md`, `data.json`, and `memory.md`\n- keep `data.json` as an authoring/thinking artifact rather than a runtime dependency\n- keep the artifact maintainable by a future agent reading only the bundle files\n\nAnti-patterns:\n\n- cursory or low-effort data collection\n- fake data added to make the artifact look complete\n- brittle one-off hacks that a later agent cannot reproduce\n- hidden dependencies that are not documented in `artifact.md` or `memory.md`\n- layout churn during simple data refreshes when the data model did not change\n\n## HTML house style\n\nHTML artifacts should feel intentional and readable inside Television tiles.\n\nTelevision provides a full base stylesheet for HTML artifacts. Only add custom\nCSS when you need something not covered by the built-in styles. Prefer the base\nstyles and theme tokens so artifacts stay visually coherent with the rest of\nTelevision.\n\nHouse-style guidance:\n\n- use semantic HTML first\n- keep the most important information near the top\n- design for small, medium, and large tile sizes\n- avoid horizontal overflow unless there is no reasonable alternative\n- make empty states and error states explicit\n- prefer the built-in HTML styling before inventing custom component chrome\n\n### Elements\n\nStandard elements already have sensible defaults, so you usually do not need to\nstyle from scratch:\n\n- headings (`h1`\u2013`h6`) \u2014 sized and weighted\n- `p`, `ul`, `ol` \u2014 readable defaults\n- `code` and `pre` \u2014 monospace, muted background\n- `blockquote` \u2014 left border, muted text\n- `table`, `th`, `td` \u2014 bordered, striped headers\n- `button` \u2014 styled with border and hover state; use `size="sm"` or `size="md"` when appropriate\n- `hr` \u2014 subtle border\n- `a` \u2014 inherits color by default\n\n### `.prose` class\n\nUse a `.prose` wrapper for document-style HTML where readable vertical rhythm is\nappropriate. Do not rely on `.prose` for dashboards, tables, control surfaces,\nor dense custom layouts.\n\n```html\n<div class="prose">\n <h1>Title</h1>\n <p>Some content with proper spacing between elements.</p>\n <ul>\n <li>Item one</li>\n <li>Item two</li>\n </ul>\n</div>\n```\n\n### CSS variables\n\nUse the existing Television tokens when they are available in the runtime.\nThese are the preferred way to stay aligned with the app theme.\n\nColors:\n- `--color-bg` \u2014 page background\n- `--color-bg-muted` \u2014 subtle background\n- `--color-surface` \u2014 card or panel background\n- `--color-text` \u2014 primary text\n- `--color-text-muted` \u2014 secondary or label text\n- `--color-border` \u2014 border color\n\nSpacing:\n- `--space-4`\n- `--space-8`\n- `--space-12`\n- `--space-16`\n- `--space-24`\n- `--space-32`\n\nFonts:\n- `--font-sans`\n- `--font-mono`\n\nText sizes:\n- `--text-sm`\n- `--text-base`\n- `--text-lg`\n- `--text-xl`\n\nRadius:\n- `--radius-4`\n- `--radius-8`\n\n## Workflows\n\n### Create new internal artifact\n\n1. Decide that the result should be an internal artifact.\n2. Start the pending bundle:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type text/markdown --title "Artifact title"\n```\n\nOr:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type text/html --title "Artifact title"\n```\n\n3. Read the returned pending path and edit files there.\n4. Write `artifact.md`.\n5. In `artifact.md`, capture the user\'s language faithfully in `## User intent` before doing the rest of the authoring work. Use direct quotes when helpful, or a close paraphrase when that is clearer, but keep it representative of what the user actually said they wanted.\n6. Think through the artifact in a pure way and write `data.json` only as an authoring aid.\n7. For markdown artifacts, leave `data.json` as `{}` unless there is a compelling authoring reason not to.\n8. Render `public/index.md` or `public/index.html`.\n9. Append a current timestamped activity entry in `memory.md`.\n10. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\n### Update internal artifact with fresh data\n\n1. Stage the edit:\n\n```bash\ntv edit-internal-artifact --id "<artifact-id>"\n```\n\n2. Read `artifact.md`, `data.json`, and `memory.md` before changing anything.\n3. Refresh the underlying facts or source material.\n4. Update `data.json` only if it helps clarify the authoring plan.\n5. For markdown artifacts, prefer to keep `data.json` as `{}`.\n6. Make the minimum rendering changes needed to keep the artifact correct.\n7. Record what changed in `memory.md`.\n8. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\nAvoid unnecessary layout or styling churn during data-only refreshes.\n\n### Modify internal artifact from user feedback\n\n1. Stage the edit:\n\n```bash\ntv edit-internal-artifact --id "<artifact-id>"\n```\n\n2. Read `artifact.md`, `data.json`, and `memory.md`.\n3. Update `artifact.md` if the user intent or non-goals changed.\n4. When the user has added feedback, complaints, corrections, or new guidance, update `## User intent` so it remains a faithful record of what the user actually wants now. Preserve the user\'s language as closely as practical, using direct quotes or close paraphrases.\n5. Update `data.json` only if it improves the authoring model of the artifact.\n6. For markdown artifacts, prefer to keep `data.json` as `{}`.\n7. Adjust `public/index.md` or `public/index.html` as narrowly as possible.\n8. Record the request, decision, and resulting change in `memory.md`.\n9. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\n### Abandon pending work\n\nIf the staged work should be discarded instead of committed:\n\n```bash\ntv abandon-pending-artifact --id "<artifact-id>"\n```\n\n### Create external artifact\n\nUse this when the file already exists on disk and Television should display it\nwithout owning a bundle:\n\n```bash\ntv create-external-artifact --screen "<screen-id>" --type text/markdown --title "Artifact title" --path /absolute/path/to/file.md\n```\n\nOr:\n\n```bash\ntv create-external-artifact --screen "<screen-id>" --type text/html --title "Artifact title" --path /absolute/path/to/file.html\n```\n\nRules:\n\n- `--path` must be absolute\n- the file must already exist and be readable\n- the extension must match `type`\n- external artifacts do not use pending create, pending edit, commit, or abandon\n\n## CLI reference\n\nWorkflow commands:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type <text/markdown|text/html> --title "Artifact title"\ntv edit-internal-artifact --id "<artifact-id>"\ntv commit-pending-artifact --id "<artifact-id>"\ntv abandon-pending-artifact --id "<artifact-id>"\ntv create-external-artifact --screen "<screen-id>" --type <text/markdown|text/html> --title "Artifact title" --path /absolute/path\ntv update-artifact --id "<artifact-id>" --title "New title"\ntv remove-artifact --id "<artifact-id>" --screen "<screen-id>"\ntv remove-screen --id "<screen-id>"\n```\n\nRead and server commands:\n\n```bash\ntv list-screens\ntv get-screen --id "<screen-id>"\ntv create-screen --name "Screen name"\ntv storage-path\ntv status\ntv serve\ntv stop\n```\n\nCLI behavior notes:\n\n- workflow and mutation commands print plain text\n- read commands print JSON\n- `tv get-screen` includes artifact `kind` and `status`\n- `tv remove-artifact` removes the artifact reference from the named screen; if another screen still references it, the artifact is unlinked and kept alive; otherwise it cascades to trash/discard\n- `tv update-artifact` changes title metadata only\n- when the CLI reports an error, follow the directive to run `tv help`\n\n## Deferred or out of scope\n\nThese are not part of the current implementation:\n\n- `tv help <topic>`\n- restore-from-trash\n- pending-listing commands\n- attestation or nonce commands\n- stale pending cleanup or stale trash cleanup\n- markdown editor UI recovery\n- client-side pending presentation work\n- multi-section help output'.length > 0) {
49593
- return '# Television\n\nTelevision is a persistent artifact screen for agents. Use it when the user\nshould be able to inspect, revisit, and refine a file-backed result instead of\nonly reading a chat reply.\n\nIf you lose context, run:\n\n```bash\ntv help\n```\n\nThat command prints this full skill as one blob. There is no topic-scoped help\nin the current implementation.\n\n## Mental model\n\n- A **screen** is the screen and layout container.\n- An **artifact** is one displayed result on that screen.\n- An **internal artifact** is a Television-managed bundle. You create a pending\n bundle, edit files in that bundle, then commit it.\n- An **external artifact** is a pointer to an existing absolute file on disk.\n Television displays that file but does not own or delete it.\n- **Pending** means a create or edit is staged but not yet committed.\n- **Trash** means metadata and committed internal bundles moved out of the live\n tree. There is no restore workflow in the current scope.\n\nThe core workflow is:\n\n1. Decide whether the result should be internal or external.\n2. Create or stage the artifact with the CLI.\n3. For internal artifacts, edit files in the pending bundle.\n4. Commit when the validation rules are satisfied.\n\n## User communication during multi-step workflows\n\nWhen you are doing a multi-step artifact workflow, keep the user informed as you\nprogress.\n\nRequired communication style:\n\n- verbalize key actions and decisions as they happen\n- keep the language concise\n- prefer short updates over long explanations\n- frame updates in the user\'s world and goals, not in the internal mechanics of the skill or CLI workflow\n- avoid technical workflow jargon unless the user explicitly asks for it\n- do not write reports, long paragraphs, or chatty summaries while the work is in progress\n- do not use lists unless the user explicitly asks for one\n- optimize for speed and token efficiency\n\nGood examples:\n\n- "Starting the artifact now."\n- "Reviewing the draft and source material."\n- "Updating the HTML and efficiently navigating the artifact creation flow."\n- "The artifact did not pass validation yet; fixing the draft notes and retrying."\n- "Finalizing the artifact now."\n- "Done."\n\nBad examples:\n\n- multi-paragraph progress reports\n- long retrospective narration during execution\n- verbose bullet lists for routine workflow steps\n\n## Internal versus external\n\nUse an **internal artifact** when:\n\n- the artifact is purpose-built for Television\n- Television should own the bundle structure\n- future agents should be able to maintain the result by reading bundle files\n- you need a staged create or staged edit workflow\n\nUse an **external artifact** when:\n\n- a real file already exists on disk\n- the user wants Television to display that existing file\n- you do not need a Television-managed bundle\n\nDecision rule:\n\n- If the result should be maintained as a Television-owned long-lived artifact,\n choose internal.\n- If the result is already a real file outside Television and should stay that\n way, choose external.\n\nSupported artifact types:\n\n- `text/markdown`\n- `text/html`\n\n## Internal bundle files\n\nEvery internal artifact bundle contains:\n\n- `artifact.md`\n- `data.json`\n- `memory.md`\n- `public/index.md` or `public/index.html`\n\nFresh pending bundles are intentionally minimal:\n\n- `artifact.md` is blank\n- `memory.md` is blank\n- `public/index.md` or `public/index.html` is blank\n- `data.json` is exactly `{}`\n\nThe scaffold is not commit-valid by itself. Learn the required structure from\nthis skill, not from placeholder content in the scaffold.\n\n### `artifact.md`\n\n`artifact.md` is the contract for the artifact. It explains what the artifact\nis for, what conceptual material it is based on, how it should render, and how\nlater agents should maintain it.\n\nBefore commit, `artifact.md` must be non-empty and contain all of these exact\nheadings:\n\n```md\n## User intent\n## Purpose\n## Data shape\n## Data sources\n## Rendering\n## Update workflow\n## Non-goals\n```\n\nWhat each section should capture:\n\n- `## User intent`: faithful restatement or quotation of what the user actually said they wanted; this is critical and should preserve the user\'s language as closely as practical, including requests, feedback, complaints, constraints, and guidance\n- `## Purpose`: what the artifact is trying to achieve\n- `## Data shape`: the conceptual shape you reasoned about while authoring; for markdown artifacts this will often just be `{}`\n- `## Data sources`: where the underlying facts, notes, or source material came from and how they were obtained\n- `## Rendering`: how `public/index.md` or `public/index.html` should present it\n- `## Update workflow`: how future agents should refresh or modify it\n- `## Non-goals`: what is intentionally excluded, especially application-like runtime behavior\n\n### `data.json`\n\n`data.json` is a **thinking artifact**, not a runtime payload.\n\nIts purpose is to help the model separate:\n\n- reasoning / planning / authoring structure\n- from final presentation in `public/index.md` or `public/index.html`\n\nUse it to capture the pure conceptual shape of what you are about to render.\nThis is an authoring aid for agents, not an application data layer.\n\nHard rules:\n\n- **Do not treat `data.json` as live runtime state.**\n- **Do not write HTML/JS that loads, depends on, or synchronizes against `data.json`.**\n- **Do not build application-like data-driven artifacts.**\n- **We do not support runtime data-backed artifacts at this time.**\n- Artifacts are static markdown or static HTML documents.\n- HTML artifacts may include JavaScript and extra assets under `public/`, but\n that JavaScript must stay presentation-oriented and self-contained, not\n driven by `data.json` as an application state container.\n\nFor `text/markdown` artifacts, leave `data.json` as exactly:\n\n```json\n{}\n```\n\nThere is usually little value in separating content from presentation for\nmarkdown artifacts, so prefer `{}` unless there is a very strong authoring\nreason not to.\n\nFor `text/html` artifacts, use `data.json` only when it helps you think clearly\nabout the material before rendering. It may describe the conceptual structure\nof the artifact, but it must not become a runtime contract.\n\nValidation rule:\n\n- `data.json` must exist and contain valid JSON\n\nThe current validator does not require the JSON value to be an object, but an\nobject is the normal choice.\n\n### `memory.md`\n\n`memory.md` is the working scratchpad for later agents. Record decisions,\nlimitations, data-retrieval notes, problems encountered, what changed, and what\nshould be watched during future edits.\n\nRequired validation anchors:\n\n- `memory.md` must contain `## Activity Log`\n- `memory.md` must contain at least one UTC timestamp in exact\n `YYYY-MM-DDTHH:MM:SSZ` format\n- at least one timestamp must be from the last 30 minutes when you commit\n\nThe minimum required heading is:\n\n```md\n## Activity Log\n```\n\nWhat to record beyond that is up to the artifact and the work performed.\n\n### `public/index.md` and `public/index.html`\n\nThis is the rendered entry file that Television serves.\n\n- Markdown artifacts use `public/index.md`\n- HTML artifacts use `public/index.html`\n- the entry file must match the artifact `type`\n- the entry file must be non-empty before commit\n\nFor HTML artifacts:\n\n- `public/index.html` is a full HTML document, not a body fragment\n- additional public assets may live under `public/`\n- keep paths relative to `public/`\n\n## Quality bar\n\nBuild artifacts that are durable, truthful, and maintainable by later agents.\n\nRequired quality standards:\n\n- be faithful to source data\n- do not invent or hallucinate missing facts\n- do not silently truncate a dataset and pretend it is complete\n- prefer truth over completeness when those goals conflict\n- make limitations, sampling, missing data, and freshness visible\n- keep rendering aligned with the reasoning captured in `artifact.md`, `data.json`, and `memory.md`\n- keep `data.json` as an authoring/thinking artifact rather than a runtime dependency\n- keep the artifact maintainable by a future agent reading only the bundle files\n\nAnti-patterns:\n\n- cursory or low-effort data collection\n- fake data added to make the artifact look complete\n- brittle one-off hacks that a later agent cannot reproduce\n- hidden dependencies that are not documented in `artifact.md` or `memory.md`\n- layout churn during simple data refreshes when the data model did not change\n\n## HTML house style\n\nHTML artifacts should feel intentional and readable inside Television tiles.\n\nTelevision provides a full base stylesheet for HTML artifacts. Only add custom\nCSS when you need something not covered by the built-in styles. Prefer the base\nstyles and theme tokens so artifacts stay visually coherent with the rest of\nTelevision.\n\nHouse-style guidance:\n\n- use semantic HTML first\n- keep the most important information near the top\n- design for small, medium, and large tile sizes\n- avoid horizontal overflow unless there is no reasonable alternative\n- make empty states and error states explicit\n- prefer the built-in HTML styling before inventing custom component chrome\n\n### Elements\n\nStandard elements already have sensible defaults, so you usually do not need to\nstyle from scratch:\n\n- headings (`h1`\u2013`h6`) \u2014 sized and weighted\n- `p`, `ul`, `ol` \u2014 readable defaults\n- `code` and `pre` \u2014 monospace, muted background\n- `blockquote` \u2014 left border, muted text\n- `table`, `th`, `td` \u2014 bordered, striped headers\n- `button` \u2014 styled with border and hover state; use `size="sm"` or `size="md"` when appropriate\n- `hr` \u2014 subtle border\n- `a` \u2014 inherits color by default\n\n### `.prose` class\n\nUse a `.prose` wrapper for document-style HTML where readable vertical rhythm is\nappropriate. Do not rely on `.prose` for dashboards, tables, control surfaces,\nor dense custom layouts.\n\n```html\n<div class="prose">\n <h1>Title</h1>\n <p>Some content with proper spacing between elements.</p>\n <ul>\n <li>Item one</li>\n <li>Item two</li>\n </ul>\n</div>\n```\n\n### CSS variables\n\nUse the existing Television tokens when they are available in the runtime.\nThese are the preferred way to stay aligned with the app theme.\n\nColors:\n- `--color-bg` \u2014 page background\n- `--color-bg-muted` \u2014 subtle background\n- `--color-surface` \u2014 card or panel background\n- `--color-text` \u2014 primary text\n- `--color-text-muted` \u2014 secondary or label text\n- `--color-border` \u2014 border color\n\nSpacing:\n- `--space-4`\n- `--space-8`\n- `--space-12`\n- `--space-16`\n- `--space-24`\n- `--space-32`\n\nFonts:\n- `--font-sans`\n- `--font-mono`\n\nText sizes:\n- `--text-sm`\n- `--text-base`\n- `--text-lg`\n- `--text-xl`\n\nRadius:\n- `--radius-4`\n- `--radius-8`\n\n## Workflows\n\n### Create new internal artifact\n\n1. Decide that the result should be an internal artifact.\n2. Start the pending bundle:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type text/markdown --title "Artifact title"\n```\n\nOr:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type text/html --title "Artifact title"\n```\n\n3. Read the returned pending path and edit files there.\n4. Write `artifact.md`.\n5. In `artifact.md`, capture the user\'s language faithfully in `## User intent` before doing the rest of the authoring work. Use direct quotes when helpful, or a close paraphrase when that is clearer, but keep it representative of what the user actually said they wanted.\n6. Think through the artifact in a pure way and write `data.json` only as an authoring aid.\n7. For markdown artifacts, leave `data.json` as `{}` unless there is a compelling authoring reason not to.\n8. Render `public/index.md` or `public/index.html`.\n9. Append a current timestamped activity entry in `memory.md`.\n10. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\n### Update internal artifact with fresh data\n\n1. Stage the edit:\n\n```bash\ntv edit-internal-artifact --id "<artifact-id>"\n```\n\n2. Read `artifact.md`, `data.json`, and `memory.md` before changing anything.\n3. Refresh the underlying facts or source material.\n4. Update `data.json` only if it helps clarify the authoring plan.\n5. For markdown artifacts, prefer to keep `data.json` as `{}`.\n6. Make the minimum rendering changes needed to keep the artifact correct.\n7. Record what changed in `memory.md`.\n8. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\nAvoid unnecessary layout or styling churn during data-only refreshes.\n\n### Modify internal artifact from user feedback\n\n1. Stage the edit:\n\n```bash\ntv edit-internal-artifact --id "<artifact-id>"\n```\n\n2. Read `artifact.md`, `data.json`, and `memory.md`.\n3. Update `artifact.md` if the user intent or non-goals changed.\n4. When the user has added feedback, complaints, corrections, or new guidance, update `## User intent` so it remains a faithful record of what the user actually wants now. Preserve the user\'s language as closely as practical, using direct quotes or close paraphrases.\n5. Update `data.json` only if it improves the authoring model of the artifact.\n6. For markdown artifacts, prefer to keep `data.json` as `{}`.\n7. Adjust `public/index.md` or `public/index.html` as narrowly as possible.\n8. Record the request, decision, and resulting change in `memory.md`.\n9. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\n### Abandon pending work\n\nIf the staged work should be discarded instead of committed:\n\n```bash\ntv abandon-pending-artifact --id "<artifact-id>"\n```\n\n### Create external artifact\n\nUse this when the file already exists on disk and Television should display it\nwithout owning a bundle:\n\n```bash\ntv create-external-artifact --screen "<screen-id>" --type text/markdown --title "Artifact title" --path /absolute/path/to/file.md\n```\n\nOr:\n\n```bash\ntv create-external-artifact --screen "<screen-id>" --type text/html --title "Artifact title" --path /absolute/path/to/file.html\n```\n\nRules:\n\n- `--path` must be absolute\n- the file must already exist and be readable\n- the extension must match `type`\n- external artifacts do not use pending create, pending edit, commit, or abandon\n\n## CLI reference\n\nWorkflow commands:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type <text/markdown|text/html> --title "Artifact title"\ntv edit-internal-artifact --id "<artifact-id>"\ntv commit-pending-artifact --id "<artifact-id>"\ntv abandon-pending-artifact --id "<artifact-id>"\ntv create-external-artifact --screen "<screen-id>" --type <text/markdown|text/html> --title "Artifact title" --path /absolute/path\ntv update-artifact --id "<artifact-id>" --title "New title"\ntv remove-artifact --id "<artifact-id>" --screen "<screen-id>"\ntv remove-screen --id "<screen-id>"\n```\n\nRead and server commands:\n\n```bash\ntv list-screens\ntv get-screen --id "<screen-id>"\ntv create-screen --name "Screen name"\ntv storage-path\ntv status\ntv serve\ntv stop\n```\n\nCLI behavior notes:\n\n- workflow and mutation commands print plain text\n- read commands print JSON\n- `tv get-screen` includes artifact `kind` and `status`\n- `tv remove-artifact` removes the artifact reference from the named screen; if another screen still references it, the artifact is unlinked and kept alive; otherwise it cascades to trash/discard\n- `tv update-artifact` changes title metadata only\n- when the CLI reports an error, follow the directive to run `tv help`\n\n## Deferred or out of scope\n\nThese are not part of the current implementation:\n\n- `tv help <topic>`\n- restore-from-trash\n- pending-listing commands\n- attestation or nonce commands\n- stale pending cleanup or stale trash cleanup\n- markdown editor UI recovery\n- client-side pending presentation work\n- multi-section help output';
50023
+ if ('# Television\n\nTelevision is a persistent artifact screen for agents. Use it when the user\nshould be able to inspect, revisit, and refine a file-backed result instead of\nonly reading a chat reply.\n\nIf you lose context, run:\n\n```bash\ntv help\n```\n\nThat command prints this full skill as one blob. There is no topic-scoped help\nin the current implementation.\n\n## Mental model\n\n- A **screen** is a named viewer surface with a layout.\n- An **artifact** is a file-backed result that can exist independently of any\n screen. It can be unplaced, attached to one screen, or attached to multiple\n screens.\n- **Screen membership** is separate from artifact identity: attaching/detaching\n controls which screens show an artifact; deleting removes the artifact\n globally. The CLI create commands require `--screen` so in-progress artifacts\n are visible immediately.\n- An **internal artifact** is a Television-managed bundle. You create a pending\n bundle, edit files in that bundle, then commit it.\n- An **external artifact** is a pointer to an existing absolute file on disk.\n Television displays that file but does not own or delete it.\n- **Pending** means a create or edit is staged but not yet committed.\n- **Trash** means metadata and committed internal bundles moved out of the live\n tree. There is no restore workflow in the current scope.\n\nThe core workflow is:\n\n1. Decide whether the result should be internal or external.\n2. Create or stage the artifact with the CLI.\n3. For internal artifacts, edit files in the pending bundle.\n4. Commit when the validation rules are satisfied.\n\n## User communication during multi-step workflows\n\nWhen you are doing a multi-step artifact workflow, keep the user informed as you\nprogress.\n\nRequired communication style:\n\n- verbalize key actions and decisions as they happen\n- keep the language concise\n- prefer short updates over long explanations\n- frame updates in the user\'s world and goals, not in the internal mechanics of the skill or CLI workflow\n- avoid technical workflow jargon unless the user explicitly asks for it\n- do not write reports, long paragraphs, or chatty summaries while the work is in progress\n- do not use lists unless the user explicitly asks for one\n- optimize for speed and token efficiency\n\nGood examples:\n\n- "Starting the artifact now."\n- "Reviewing the draft and source material."\n- "Updating the HTML and efficiently navigating the artifact creation flow."\n- "The artifact did not pass validation yet; fixing the draft notes and retrying."\n- "Finalizing the artifact now."\n- "Done."\n\nBad examples:\n\n- multi-paragraph progress reports\n- long retrospective narration during execution\n- verbose bullet lists for routine workflow steps\n\n## Internal versus external\n\nUse an **internal artifact** when:\n\n- the artifact is purpose-built for Television\n- Television should own the bundle structure\n- future agents should be able to maintain the result by reading bundle files\n- you need a staged create or staged edit workflow\n\nUse an **external artifact** when:\n\n- a real file already exists on disk\n- the user wants Television to display that existing file\n- you do not need a Television-managed bundle\n\nDecision rule:\n\n- If the result should be maintained as a Television-owned long-lived artifact,\n choose internal.\n- If the result is already a real file outside Television and should stay that\n way, choose external.\n\nSupported artifact types:\n\n- `text/markdown`\n- `text/html`\n\n## Internal bundle files\n\nEvery internal artifact bundle contains:\n\n- `artifact.md`\n- `data.json`\n- `memory.md`\n- `public/index.md` or `public/index.html`\n\nFresh pending bundles are intentionally minimal:\n\n- `artifact.md` is blank\n- `memory.md` is blank\n- `public/index.md` or `public/index.html` is blank\n- `data.json` is exactly `{}`\n\nThe scaffold is not commit-valid by itself. Learn the required structure from\nthis skill, not from placeholder content in the scaffold.\n\n### `artifact.md`\n\n`artifact.md` is the contract for the artifact. It explains what the artifact\nis for, what conceptual material it is based on, how it should render, and how\nlater agents should maintain it.\n\nBefore commit, `artifact.md` must be non-empty and contain all of these exact\nheadings:\n\n```md\n## User intent\n## Purpose\n## Data shape\n## Data sources\n## Rendering\n## Update workflow\n## Non-goals\n```\n\nWhat each section should capture:\n\n- `## User intent`: faithful restatement or quotation of what the user actually said they wanted; this is critical and should preserve the user\'s language as closely as practical, including requests, feedback, complaints, constraints, and guidance\n- `## Purpose`: what the artifact is trying to achieve\n- `## Data shape`: the conceptual shape you reasoned about while authoring; for markdown artifacts this will often just be `{}`\n- `## Data sources`: where the underlying facts, notes, or source material came from and how they were obtained\n- `## Rendering`: how `public/index.md` or `public/index.html` should present it\n- `## Update workflow`: how future agents should refresh or modify it\n- `## Non-goals`: what is intentionally excluded, especially application-like runtime behavior\n\n### `data.json`\n\n`data.json` is a **thinking artifact**, not a runtime payload.\n\nIts purpose is to help the model separate:\n\n- reasoning / planning / authoring structure\n- from final presentation in `public/index.md` or `public/index.html`\n\nUse it to capture the pure conceptual shape of what you are about to render.\nThis is an authoring aid for agents, not an application data layer.\n\nHard rules:\n\n- **Do not treat `data.json` as live runtime state.**\n- **Do not write HTML/JS that loads, depends on, or synchronizes against `data.json`.**\n- **Do not build application-like data-driven artifacts.**\n- **We do not support runtime data-backed artifacts at this time.**\n- Artifacts are static markdown or static HTML documents.\n- HTML artifacts may include JavaScript and extra assets under `public/`, but\n that JavaScript must stay presentation-oriented and self-contained, not\n driven by `data.json` as an application state container.\n\nFor `text/markdown` artifacts, leave `data.json` as exactly:\n\n```json\n{}\n```\n\nThere is usually little value in separating content from presentation for\nmarkdown artifacts, so prefer `{}` unless there is a very strong authoring\nreason not to.\n\nFor `text/html` artifacts, use `data.json` only when it helps you think clearly\nabout the material before rendering. It may describe the conceptual structure\nof the artifact, but it must not become a runtime contract.\n\nValidation rule:\n\n- `data.json` must exist and contain valid JSON\n\nThe current validator does not require the JSON value to be an object, but an\nobject is the normal choice.\n\n### `memory.md`\n\n`memory.md` is the working scratchpad for later agents. Record decisions,\nlimitations, data-retrieval notes, problems encountered, what changed, and what\nshould be watched during future edits.\n\nRequired validation anchors:\n\n- `memory.md` must contain `## Activity Log`\n- `memory.md` must contain at least one UTC timestamp in exact\n `YYYY-MM-DDTHH:MM:SSZ` format\n- at least one timestamp must be from the last 30 minutes when you commit\n\nThe minimum required heading is:\n\n```md\n## Activity Log\n```\n\nWhat to record beyond that is up to the artifact and the work performed.\n\n### `public/index.md` and `public/index.html`\n\nThis is the rendered entry file that Television serves.\n\n- Markdown artifacts use `public/index.md`\n- HTML artifacts use `public/index.html`\n- the entry file must match the artifact `type`\n- the entry file must be non-empty before commit\n\nFor HTML artifacts:\n\n- `public/index.html` is a full HTML document, not a body fragment\n- additional public assets may live under `public/`\n- keep paths relative to `public/`\n\n## Quality bar\n\nBuild artifacts that are durable, truthful, and maintainable by later agents.\n\nRequired quality standards:\n\n- be faithful to source data\n- do not invent or hallucinate missing facts\n- do not silently truncate a dataset and pretend it is complete\n- prefer truth over completeness when those goals conflict\n- make limitations, sampling, missing data, and freshness visible\n- keep rendering aligned with the reasoning captured in `artifact.md`, `data.json`, and `memory.md`\n- keep `data.json` as an authoring/thinking artifact rather than a runtime dependency\n- keep the artifact maintainable by a future agent reading only the bundle files\n\nAnti-patterns:\n\n- cursory or low-effort data collection\n- fake data added to make the artifact look complete\n- brittle one-off hacks that a later agent cannot reproduce\n- hidden dependencies that are not documented in `artifact.md` or `memory.md`\n- layout churn during simple data refreshes when the data model did not change\n\n## HTML house style\n\nHTML artifacts should feel intentional and readable inside Television tiles.\n\nTelevision provides a full base stylesheet for HTML artifacts. Only add custom\nCSS when you need something not covered by the built-in styles. Prefer the base\nstyles and theme tokens so artifacts stay visually coherent with the rest of\nTelevision.\n\nHouse-style guidance:\n\n- use semantic HTML first\n- keep the most important information near the top\n- design for small, medium, and large tile sizes\n- avoid horizontal overflow unless there is no reasonable alternative\n- make empty states and error states explicit\n- prefer the built-in HTML styling before inventing custom component chrome\n\n### Elements\n\nStandard elements already have sensible defaults, so you usually do not need to\nstyle from scratch:\n\n- headings (`h1`\u2013`h6`) \u2014 sized and weighted\n- `p`, `ul`, `ol` \u2014 readable defaults\n- `code` and `pre` \u2014 monospace, muted background\n- `blockquote` \u2014 left border, muted text\n- `table`, `th`, `td` \u2014 bordered, striped headers\n- `button` \u2014 styled with border and hover state; use `size="sm"` or `size="md"` when appropriate\n- `hr` \u2014 subtle border\n- `a` \u2014 inherits color by default\n\n### `.prose` class\n\nUse a `.prose` wrapper for document-style HTML where readable vertical rhythm is\nappropriate. Do not rely on `.prose` for dashboards, tables, control surfaces,\nor dense custom layouts.\n\n```html\n<div class="prose">\n <h1>Title</h1>\n <p>Some content with proper spacing between elements.</p>\n <ul>\n <li>Item one</li>\n <li>Item two</li>\n </ul>\n</div>\n```\n\n### CSS variables\n\nUse the existing Television tokens when they are available in the runtime.\nThese are the preferred way to stay aligned with the app theme.\n\nColors:\n- `--color-bg` \u2014 page background\n- `--color-bg-muted` \u2014 subtle background\n- `--color-surface` \u2014 card or panel background\n- `--color-text` \u2014 primary text\n- `--color-text-muted` \u2014 secondary or label text\n- `--color-border` \u2014 border color\n\nSpacing:\n- `--space-4`\n- `--space-8`\n- `--space-12`\n- `--space-16`\n- `--space-24`\n- `--space-32`\n\nFonts:\n- `--font-sans`\n- `--font-mono`\n\nText sizes:\n- `--text-sm`\n- `--text-base`\n- `--text-lg`\n- `--text-xl`\n\nRadius:\n- `--radius-4`\n- `--radius-8`\n\n## Workflows\n\n### Create new internal artifact\n\n1. Decide that the result should be an internal artifact.\n2. Start the pending bundle:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type text/markdown --title "Artifact title"\n```\n\nOr:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type text/html --title "Artifact title"\n```\n\n`--screen` is required for internal artifact creation so the user can see the in-progress artifact immediately.\n\n3. Read the returned pending path and edit files there.\n4. Write `artifact.md`.\n5. In `artifact.md`, capture the user\'s language faithfully in `## User intent` before doing the rest of the authoring work. Use direct quotes when helpful, or a close paraphrase when that is clearer, but keep it representative of what the user actually said they wanted.\n6. Think through the artifact in a pure way and write `data.json` only as an authoring aid.\n7. For markdown artifacts, leave `data.json` as `{}` unless there is a compelling authoring reason not to.\n8. Render `public/index.md` or `public/index.html`.\n9. Append a current timestamped activity entry in `memory.md`.\n10. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\n### Update internal artifact with fresh data\n\n1. Stage the edit:\n\n```bash\ntv edit-internal-artifact --id "<artifact-id>"\n```\n\n2. Read `artifact.md`, `data.json`, and `memory.md` before changing anything.\n3. Refresh the underlying facts or source material.\n4. Update `data.json` only if it helps clarify the authoring plan.\n5. For markdown artifacts, prefer to keep `data.json` as `{}`.\n6. Make the minimum rendering changes needed to keep the artifact correct.\n7. Record what changed in `memory.md`.\n8. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\nAvoid unnecessary layout or styling churn during data-only refreshes.\n\n### Modify internal artifact from user feedback\n\n1. Stage the edit:\n\n```bash\ntv edit-internal-artifact --id "<artifact-id>"\n```\n\n2. Read `artifact.md`, `data.json`, and `memory.md`.\n3. Update `artifact.md` if the user intent or non-goals changed.\n4. When the user has added feedback, complaints, corrections, or new guidance, update `## User intent` so it remains a faithful record of what the user actually wants now. Preserve the user\'s language as closely as practical, using direct quotes or close paraphrases.\n5. Update `data.json` only if it improves the authoring model of the artifact.\n6. For markdown artifacts, prefer to keep `data.json` as `{}`.\n7. Adjust `public/index.md` or `public/index.html` as narrowly as possible.\n8. Record the request, decision, and resulting change in `memory.md`.\n9. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\n### Abandon pending work\n\nIf the staged work should be discarded instead of committed:\n\n```bash\ntv abandon-pending-artifact --id "<artifact-id>"\n```\n\n### Create external artifact\n\nUse this when the file already exists on disk and Television should display it\nwithout owning a bundle:\n\n```bash\ntv create-external-artifact --screen "<screen-id>" --type text/markdown --title "Artifact title" --path /absolute/path/to/file.md\n```\n\nOr:\n\n```bash\ntv create-external-artifact --screen "<screen-id>" --type text/html --title "Artifact title" --path /absolute/path/to/file.html\n```\n\n`--screen` is required for CLI creation so the file appears on a visible screen immediately.\n\nRules:\n\n- `--path` must be absolute\n- the file must already exist and be readable\n- the extension must match `type`\n- external artifacts do not use pending create, pending edit, commit, or abandon\n\n## CLI reference\n\nArtifact commands:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type <text/markdown|text/html> --title "Artifact title"\ntv create-external-artifact --screen "<screen-id>" --type <text/markdown|text/html> --title "Artifact title" --path /absolute/path\ntv edit-internal-artifact --id "<artifact-id>"\ntv commit-pending-artifact --id "<artifact-id>"\ntv abandon-pending-artifact --id "<artifact-id>"\ntv update-artifact --id "<artifact-id>" --title "New title"\ntv list-artifacts [--screen "<screen-id>"] [--unplaced]\ntv get-artifact --id "<artifact-id>"\ntv delete-artifact --id "<artifact-id>"\n```\n\nScreen commands:\n\n```bash\ntv create-screen --name "Screen name"\ntv list-screens\ntv get-screen --id "<screen-id>"\ntv remove-screen --id "<screen-id>"\n```\n\nScreen membership commands:\n\n```bash\ntv attach-artifact --id "<artifact-id>" --screen "<screen-id>"\ntv detach-artifact --id "<artifact-id>" --screen "<screen-id>"\n```\n\nViewer commands:\n\n```bash\ntv viewer-status\ntv set-active-screen --id "<screen-id>"\ntv focus-artifact --id "<artifact-id>" [--screen "<screen-id>"]\n```\n\nServer commands:\n\n```bash\ntv status\ntv storage-path\ntv serve\ntv stop\n```\n\nCLI behavior notes:\n\n- `--screen` is required on CLI create commands so new artifacts are visible immediately; use `tv attach-artifact` and `tv detach-artifact` for later screen membership changes\n- workflow and mutation commands print plain text\n- read commands print JSON\n- `tv get-screen` includes artifact `kind` and `status`\n- `tv attach-artifact` appends a default-sized card to the right end of the strip; idempotent if the artifact is already on that screen\n- `tv detach-artifact` removes the card from a screen\'s layout; the artifact metadata is never touched, even on the last reference\n- `tv delete-artifact` is the way to globally remove an artifact (detaches from every screen, then trashes the bundle / forgets the external pointer / discards pending-create)\n- `tv list-artifacts` accepts `--screen <id>` to filter by screen membership and `--unplaced` to surface artifacts attached to no screen\n- `tv update-artifact` changes title metadata only\n- `tv set-active-screen` sets which screen the GUI is focused on; the change is persisted and broadcast to connected clients\n- `tv focus-artifact` is a transient nudge: clients switch screens if needed, scroll the artifact\'s card into view, and play a brief highlight animation; pass `--screen <id>` to pin which screen, otherwise the server picks one (preferring the active screen when the artifact is there)\n- `tv viewer-status` prints the active screen ID and the count of connected GUI clients\n- when the CLI reports an error, follow the directive to run `tv help`\n\n## Deferred or out of scope\n\nThese are not part of the current implementation:\n\n- `tv help <topic>`\n- restore-from-trash\n- pending-listing commands\n- attestation or nonce commands\n- stale pending cleanup or stale trash cleanup\n- markdown editor UI recovery\n- client-side pending presentation work\n- multi-section help output'.length > 0) {
50024
+ return '# Television\n\nTelevision is a persistent artifact screen for agents. Use it when the user\nshould be able to inspect, revisit, and refine a file-backed result instead of\nonly reading a chat reply.\n\nIf you lose context, run:\n\n```bash\ntv help\n```\n\nThat command prints this full skill as one blob. There is no topic-scoped help\nin the current implementation.\n\n## Mental model\n\n- A **screen** is a named viewer surface with a layout.\n- An **artifact** is a file-backed result that can exist independently of any\n screen. It can be unplaced, attached to one screen, or attached to multiple\n screens.\n- **Screen membership** is separate from artifact identity: attaching/detaching\n controls which screens show an artifact; deleting removes the artifact\n globally. The CLI create commands require `--screen` so in-progress artifacts\n are visible immediately.\n- An **internal artifact** is a Television-managed bundle. You create a pending\n bundle, edit files in that bundle, then commit it.\n- An **external artifact** is a pointer to an existing absolute file on disk.\n Television displays that file but does not own or delete it.\n- **Pending** means a create or edit is staged but not yet committed.\n- **Trash** means metadata and committed internal bundles moved out of the live\n tree. There is no restore workflow in the current scope.\n\nThe core workflow is:\n\n1. Decide whether the result should be internal or external.\n2. Create or stage the artifact with the CLI.\n3. For internal artifacts, edit files in the pending bundle.\n4. Commit when the validation rules are satisfied.\n\n## User communication during multi-step workflows\n\nWhen you are doing a multi-step artifact workflow, keep the user informed as you\nprogress.\n\nRequired communication style:\n\n- verbalize key actions and decisions as they happen\n- keep the language concise\n- prefer short updates over long explanations\n- frame updates in the user\'s world and goals, not in the internal mechanics of the skill or CLI workflow\n- avoid technical workflow jargon unless the user explicitly asks for it\n- do not write reports, long paragraphs, or chatty summaries while the work is in progress\n- do not use lists unless the user explicitly asks for one\n- optimize for speed and token efficiency\n\nGood examples:\n\n- "Starting the artifact now."\n- "Reviewing the draft and source material."\n- "Updating the HTML and efficiently navigating the artifact creation flow."\n- "The artifact did not pass validation yet; fixing the draft notes and retrying."\n- "Finalizing the artifact now."\n- "Done."\n\nBad examples:\n\n- multi-paragraph progress reports\n- long retrospective narration during execution\n- verbose bullet lists for routine workflow steps\n\n## Internal versus external\n\nUse an **internal artifact** when:\n\n- the artifact is purpose-built for Television\n- Television should own the bundle structure\n- future agents should be able to maintain the result by reading bundle files\n- you need a staged create or staged edit workflow\n\nUse an **external artifact** when:\n\n- a real file already exists on disk\n- the user wants Television to display that existing file\n- you do not need a Television-managed bundle\n\nDecision rule:\n\n- If the result should be maintained as a Television-owned long-lived artifact,\n choose internal.\n- If the result is already a real file outside Television and should stay that\n way, choose external.\n\nSupported artifact types:\n\n- `text/markdown`\n- `text/html`\n\n## Internal bundle files\n\nEvery internal artifact bundle contains:\n\n- `artifact.md`\n- `data.json`\n- `memory.md`\n- `public/index.md` or `public/index.html`\n\nFresh pending bundles are intentionally minimal:\n\n- `artifact.md` is blank\n- `memory.md` is blank\n- `public/index.md` or `public/index.html` is blank\n- `data.json` is exactly `{}`\n\nThe scaffold is not commit-valid by itself. Learn the required structure from\nthis skill, not from placeholder content in the scaffold.\n\n### `artifact.md`\n\n`artifact.md` is the contract for the artifact. It explains what the artifact\nis for, what conceptual material it is based on, how it should render, and how\nlater agents should maintain it.\n\nBefore commit, `artifact.md` must be non-empty and contain all of these exact\nheadings:\n\n```md\n## User intent\n## Purpose\n## Data shape\n## Data sources\n## Rendering\n## Update workflow\n## Non-goals\n```\n\nWhat each section should capture:\n\n- `## User intent`: faithful restatement or quotation of what the user actually said they wanted; this is critical and should preserve the user\'s language as closely as practical, including requests, feedback, complaints, constraints, and guidance\n- `## Purpose`: what the artifact is trying to achieve\n- `## Data shape`: the conceptual shape you reasoned about while authoring; for markdown artifacts this will often just be `{}`\n- `## Data sources`: where the underlying facts, notes, or source material came from and how they were obtained\n- `## Rendering`: how `public/index.md` or `public/index.html` should present it\n- `## Update workflow`: how future agents should refresh or modify it\n- `## Non-goals`: what is intentionally excluded, especially application-like runtime behavior\n\n### `data.json`\n\n`data.json` is a **thinking artifact**, not a runtime payload.\n\nIts purpose is to help the model separate:\n\n- reasoning / planning / authoring structure\n- from final presentation in `public/index.md` or `public/index.html`\n\nUse it to capture the pure conceptual shape of what you are about to render.\nThis is an authoring aid for agents, not an application data layer.\n\nHard rules:\n\n- **Do not treat `data.json` as live runtime state.**\n- **Do not write HTML/JS that loads, depends on, or synchronizes against `data.json`.**\n- **Do not build application-like data-driven artifacts.**\n- **We do not support runtime data-backed artifacts at this time.**\n- Artifacts are static markdown or static HTML documents.\n- HTML artifacts may include JavaScript and extra assets under `public/`, but\n that JavaScript must stay presentation-oriented and self-contained, not\n driven by `data.json` as an application state container.\n\nFor `text/markdown` artifacts, leave `data.json` as exactly:\n\n```json\n{}\n```\n\nThere is usually little value in separating content from presentation for\nmarkdown artifacts, so prefer `{}` unless there is a very strong authoring\nreason not to.\n\nFor `text/html` artifacts, use `data.json` only when it helps you think clearly\nabout the material before rendering. It may describe the conceptual structure\nof the artifact, but it must not become a runtime contract.\n\nValidation rule:\n\n- `data.json` must exist and contain valid JSON\n\nThe current validator does not require the JSON value to be an object, but an\nobject is the normal choice.\n\n### `memory.md`\n\n`memory.md` is the working scratchpad for later agents. Record decisions,\nlimitations, data-retrieval notes, problems encountered, what changed, and what\nshould be watched during future edits.\n\nRequired validation anchors:\n\n- `memory.md` must contain `## Activity Log`\n- `memory.md` must contain at least one UTC timestamp in exact\n `YYYY-MM-DDTHH:MM:SSZ` format\n- at least one timestamp must be from the last 30 minutes when you commit\n\nThe minimum required heading is:\n\n```md\n## Activity Log\n```\n\nWhat to record beyond that is up to the artifact and the work performed.\n\n### `public/index.md` and `public/index.html`\n\nThis is the rendered entry file that Television serves.\n\n- Markdown artifacts use `public/index.md`\n- HTML artifacts use `public/index.html`\n- the entry file must match the artifact `type`\n- the entry file must be non-empty before commit\n\nFor HTML artifacts:\n\n- `public/index.html` is a full HTML document, not a body fragment\n- additional public assets may live under `public/`\n- keep paths relative to `public/`\n\n## Quality bar\n\nBuild artifacts that are durable, truthful, and maintainable by later agents.\n\nRequired quality standards:\n\n- be faithful to source data\n- do not invent or hallucinate missing facts\n- do not silently truncate a dataset and pretend it is complete\n- prefer truth over completeness when those goals conflict\n- make limitations, sampling, missing data, and freshness visible\n- keep rendering aligned with the reasoning captured in `artifact.md`, `data.json`, and `memory.md`\n- keep `data.json` as an authoring/thinking artifact rather than a runtime dependency\n- keep the artifact maintainable by a future agent reading only the bundle files\n\nAnti-patterns:\n\n- cursory or low-effort data collection\n- fake data added to make the artifact look complete\n- brittle one-off hacks that a later agent cannot reproduce\n- hidden dependencies that are not documented in `artifact.md` or `memory.md`\n- layout churn during simple data refreshes when the data model did not change\n\n## HTML house style\n\nHTML artifacts should feel intentional and readable inside Television tiles.\n\nTelevision provides a full base stylesheet for HTML artifacts. Only add custom\nCSS when you need something not covered by the built-in styles. Prefer the base\nstyles and theme tokens so artifacts stay visually coherent with the rest of\nTelevision.\n\nHouse-style guidance:\n\n- use semantic HTML first\n- keep the most important information near the top\n- design for small, medium, and large tile sizes\n- avoid horizontal overflow unless there is no reasonable alternative\n- make empty states and error states explicit\n- prefer the built-in HTML styling before inventing custom component chrome\n\n### Elements\n\nStandard elements already have sensible defaults, so you usually do not need to\nstyle from scratch:\n\n- headings (`h1`\u2013`h6`) \u2014 sized and weighted\n- `p`, `ul`, `ol` \u2014 readable defaults\n- `code` and `pre` \u2014 monospace, muted background\n- `blockquote` \u2014 left border, muted text\n- `table`, `th`, `td` \u2014 bordered, striped headers\n- `button` \u2014 styled with border and hover state; use `size="sm"` or `size="md"` when appropriate\n- `hr` \u2014 subtle border\n- `a` \u2014 inherits color by default\n\n### `.prose` class\n\nUse a `.prose` wrapper for document-style HTML where readable vertical rhythm is\nappropriate. Do not rely on `.prose` for dashboards, tables, control surfaces,\nor dense custom layouts.\n\n```html\n<div class="prose">\n <h1>Title</h1>\n <p>Some content with proper spacing between elements.</p>\n <ul>\n <li>Item one</li>\n <li>Item two</li>\n </ul>\n</div>\n```\n\n### CSS variables\n\nUse the existing Television tokens when they are available in the runtime.\nThese are the preferred way to stay aligned with the app theme.\n\nColors:\n- `--color-bg` \u2014 page background\n- `--color-bg-muted` \u2014 subtle background\n- `--color-surface` \u2014 card or panel background\n- `--color-text` \u2014 primary text\n- `--color-text-muted` \u2014 secondary or label text\n- `--color-border` \u2014 border color\n\nSpacing:\n- `--space-4`\n- `--space-8`\n- `--space-12`\n- `--space-16`\n- `--space-24`\n- `--space-32`\n\nFonts:\n- `--font-sans`\n- `--font-mono`\n\nText sizes:\n- `--text-sm`\n- `--text-base`\n- `--text-lg`\n- `--text-xl`\n\nRadius:\n- `--radius-4`\n- `--radius-8`\n\n## Workflows\n\n### Create new internal artifact\n\n1. Decide that the result should be an internal artifact.\n2. Start the pending bundle:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type text/markdown --title "Artifact title"\n```\n\nOr:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type text/html --title "Artifact title"\n```\n\n`--screen` is required for internal artifact creation so the user can see the in-progress artifact immediately.\n\n3. Read the returned pending path and edit files there.\n4. Write `artifact.md`.\n5. In `artifact.md`, capture the user\'s language faithfully in `## User intent` before doing the rest of the authoring work. Use direct quotes when helpful, or a close paraphrase when that is clearer, but keep it representative of what the user actually said they wanted.\n6. Think through the artifact in a pure way and write `data.json` only as an authoring aid.\n7. For markdown artifacts, leave `data.json` as `{}` unless there is a compelling authoring reason not to.\n8. Render `public/index.md` or `public/index.html`.\n9. Append a current timestamped activity entry in `memory.md`.\n10. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\n### Update internal artifact with fresh data\n\n1. Stage the edit:\n\n```bash\ntv edit-internal-artifact --id "<artifact-id>"\n```\n\n2. Read `artifact.md`, `data.json`, and `memory.md` before changing anything.\n3. Refresh the underlying facts or source material.\n4. Update `data.json` only if it helps clarify the authoring plan.\n5. For markdown artifacts, prefer to keep `data.json` as `{}`.\n6. Make the minimum rendering changes needed to keep the artifact correct.\n7. Record what changed in `memory.md`.\n8. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\nAvoid unnecessary layout or styling churn during data-only refreshes.\n\n### Modify internal artifact from user feedback\n\n1. Stage the edit:\n\n```bash\ntv edit-internal-artifact --id "<artifact-id>"\n```\n\n2. Read `artifact.md`, `data.json`, and `memory.md`.\n3. Update `artifact.md` if the user intent or non-goals changed.\n4. When the user has added feedback, complaints, corrections, or new guidance, update `## User intent` so it remains a faithful record of what the user actually wants now. Preserve the user\'s language as closely as practical, using direct quotes or close paraphrases.\n5. Update `data.json` only if it improves the authoring model of the artifact.\n6. For markdown artifacts, prefer to keep `data.json` as `{}`.\n7. Adjust `public/index.md` or `public/index.html` as narrowly as possible.\n8. Record the request, decision, and resulting change in `memory.md`.\n9. Commit:\n\n```bash\ntv commit-pending-artifact --id "<artifact-id>"\n```\n\n### Abandon pending work\n\nIf the staged work should be discarded instead of committed:\n\n```bash\ntv abandon-pending-artifact --id "<artifact-id>"\n```\n\n### Create external artifact\n\nUse this when the file already exists on disk and Television should display it\nwithout owning a bundle:\n\n```bash\ntv create-external-artifact --screen "<screen-id>" --type text/markdown --title "Artifact title" --path /absolute/path/to/file.md\n```\n\nOr:\n\n```bash\ntv create-external-artifact --screen "<screen-id>" --type text/html --title "Artifact title" --path /absolute/path/to/file.html\n```\n\n`--screen` is required for CLI creation so the file appears on a visible screen immediately.\n\nRules:\n\n- `--path` must be absolute\n- the file must already exist and be readable\n- the extension must match `type`\n- external artifacts do not use pending create, pending edit, commit, or abandon\n\n## CLI reference\n\nArtifact commands:\n\n```bash\ntv create-internal-artifact --screen "<screen-id>" --type <text/markdown|text/html> --title "Artifact title"\ntv create-external-artifact --screen "<screen-id>" --type <text/markdown|text/html> --title "Artifact title" --path /absolute/path\ntv edit-internal-artifact --id "<artifact-id>"\ntv commit-pending-artifact --id "<artifact-id>"\ntv abandon-pending-artifact --id "<artifact-id>"\ntv update-artifact --id "<artifact-id>" --title "New title"\ntv list-artifacts [--screen "<screen-id>"] [--unplaced]\ntv get-artifact --id "<artifact-id>"\ntv delete-artifact --id "<artifact-id>"\n```\n\nScreen commands:\n\n```bash\ntv create-screen --name "Screen name"\ntv list-screens\ntv get-screen --id "<screen-id>"\ntv remove-screen --id "<screen-id>"\n```\n\nScreen membership commands:\n\n```bash\ntv attach-artifact --id "<artifact-id>" --screen "<screen-id>"\ntv detach-artifact --id "<artifact-id>" --screen "<screen-id>"\n```\n\nViewer commands:\n\n```bash\ntv viewer-status\ntv set-active-screen --id "<screen-id>"\ntv focus-artifact --id "<artifact-id>" [--screen "<screen-id>"]\n```\n\nServer commands:\n\n```bash\ntv status\ntv storage-path\ntv serve\ntv stop\n```\n\nCLI behavior notes:\n\n- `--screen` is required on CLI create commands so new artifacts are visible immediately; use `tv attach-artifact` and `tv detach-artifact` for later screen membership changes\n- workflow and mutation commands print plain text\n- read commands print JSON\n- `tv get-screen` includes artifact `kind` and `status`\n- `tv attach-artifact` appends a default-sized card to the right end of the strip; idempotent if the artifact is already on that screen\n- `tv detach-artifact` removes the card from a screen\'s layout; the artifact metadata is never touched, even on the last reference\n- `tv delete-artifact` is the way to globally remove an artifact (detaches from every screen, then trashes the bundle / forgets the external pointer / discards pending-create)\n- `tv list-artifacts` accepts `--screen <id>` to filter by screen membership and `--unplaced` to surface artifacts attached to no screen\n- `tv update-artifact` changes title metadata only\n- `tv set-active-screen` sets which screen the GUI is focused on; the change is persisted and broadcast to connected clients\n- `tv focus-artifact` is a transient nudge: clients switch screens if needed, scroll the artifact\'s card into view, and play a brief highlight animation; pass `--screen <id>` to pin which screen, otherwise the server picks one (preferring the active screen when the artifact is there)\n- `tv viewer-status` prints the active screen ID and the count of connected GUI clients\n- when the CLI reports an error, follow the directive to run `tv help`\n\n## Deferred or out of scope\n\nThese are not part of the current implementation:\n\n- `tv help <topic>`\n- restore-from-trash\n- pending-listing commands\n- attestation or nonce commands\n- stale pending cleanup or stale trash cleanup\n- markdown editor UI recovery\n- client-side pending presentation work\n- multi-section help output';
49594
50025
  }
49595
50026
  const devSkillPath = import_node_path7.default.join(getDevPackageDir(), "skill/SKILL.md");
49596
50027
  if (!(0, import_node_fs4.existsSync)(devSkillPath)) {
@@ -49667,31 +50098,7 @@ function validateExternalArtifactPath(inputPath) {
49667
50098
  }
49668
50099
  return inputPath;
49669
50100
  }
49670
- function formatArtifactRemovalResult(result, context) {
49671
- if (context.kind === "top-level") {
49672
- const sID = context.screenID;
49673
- if (result.outcome === "unlinked") {
49674
- return [`Artifact ${result.artifactID} unlinked from screen ${sID} (still referenced elsewhere).`];
49675
- }
49676
- if (result.outcome === "discarded") {
49677
- return [
49678
- `Pending-create artifact ${result.artifactID} unlinked from screen ${sID} and discarded.`,
49679
- "No trash paths were written because no committed artifact existed yet."
49680
- ];
49681
- }
49682
- if ("bundlePath" in result) {
49683
- return [
49684
- `Internal artifact ${result.artifactID} unlinked from screen ${sID}; last reference, moved to trash:`,
49685
- ` metadata: ${result.metadataPath}`,
49686
- ` bundle: ${result.bundlePath}`
49687
- ];
49688
- }
49689
- return [
49690
- `External artifact ${result.artifactID} unlinked from screen ${sID}; last reference, moved to trash:`,
49691
- ` metadata: ${result.metadataPath}`,
49692
- ` external file ${result.externalFilePath} not touched.`
49693
- ];
49694
- }
50101
+ function formatArtifactRemovalResult(result) {
49695
50102
  if (result.outcome === "unlinked") {
49696
50103
  return [` ${result.artifactID}: unlinked (still referenced elsewhere)`];
49697
50104
  }
@@ -49711,6 +50118,26 @@ function formatArtifactRemovalResult(result, context) {
49711
50118
  ` external file ${result.externalFilePath} not touched.`
49712
50119
  ];
49713
50120
  }
50121
+ function formatDeleteArtifactResult(result) {
50122
+ if (result.outcome === "discarded") {
50123
+ return [
50124
+ `Pending-create artifact ${result.artifactID} discarded.`,
50125
+ "No trash paths were written because no committed artifact existed yet."
50126
+ ];
50127
+ }
50128
+ if ("bundlePath" in result) {
50129
+ return [
50130
+ `Internal artifact ${result.artifactID} moved to trash:`,
50131
+ ` metadata: ${result.metadataPath}`,
50132
+ ` bundle: ${result.bundlePath}`
50133
+ ];
50134
+ }
50135
+ return [
50136
+ `External artifact ${result.artifactID} moved to trash:`,
50137
+ ` metadata: ${result.metadataPath}`,
50138
+ ` external file ${result.externalFilePath} not touched.`
50139
+ ];
50140
+ }
49714
50141
  function formatScreenRemovalResult(result) {
49715
50142
  const header = [
49716
50143
  `Screen ${result.screenID} moved to trash:`,
@@ -49722,7 +50149,7 @@ function formatScreenRemovalResult(result) {
49722
50149
  }
49723
50150
  header.push(`${result.artifactResults.length} referenced artifact(s):`);
49724
50151
  for (const artifactResult of result.artifactResults) {
49725
- header.push(...formatArtifactRemovalResult(artifactResult, { kind: "cascade" }));
50152
+ header.push(...formatArtifactRemovalResult(artifactResult));
49726
50153
  }
49727
50154
  return header;
49728
50155
  }
@@ -49924,13 +50351,50 @@ If you wish to display temporary content to the user, use an internal artifact i
49924
50351
  await client.artifacts.update({ artifactID: opts.id, title: opts.title });
49925
50352
  writeLine(env.stdout, `Artifact ${opts.id} updated.`);
49926
50353
  });
49927
- program2.command("remove-artifact").description("Remove an artifact reference from a screen").requiredOption("--id <id>", "Artifact ID").requiredOption("--screen <id>", "Screen ID").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
50354
+ const detachAction = async (opts) => {
49928
50355
  const client = createAuthenticatedClient(opts.server);
49929
- const result = await client.artifacts.remove({ artifactID: opts.id, screenID: opts.screen });
49930
- for (const line of formatArtifactRemovalResult(result, { kind: "top-level", screenID: opts.screen })) {
50356
+ const result = await client.artifacts.detach({
50357
+ artifactID: opts.id,
50358
+ screenID: opts.screen
50359
+ });
50360
+ writeLine(env.stdout, `Artifact ${result.artifactID} detached from screen ${result.screenID}.`);
50361
+ };
50362
+ program2.command("detach-artifact").description("Remove an artifact card from a screen's layout (artifact metadata is preserved)").requiredOption("--id <id>", "Artifact ID").requiredOption("--screen <id>", "Screen ID").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(detachAction);
50363
+ program2.command("remove-artifact", { hidden: true }).description("Deprecated alias for detach-artifact").requiredOption("--id <id>", "Artifact ID").requiredOption("--screen <id>", "Screen ID").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
50364
+ env.stderr.write(
50365
+ "Deprecated: 'tv remove-artifact' has been renamed to 'tv detach-artifact'. Note: detach no longer trashes the artifact even on the last reference; use 'tv delete-artifact' to remove an artifact globally.\n"
50366
+ );
50367
+ await detachAction(opts);
50368
+ });
50369
+ program2.command("attach-artifact").description("Attach an existing artifact to a screen by appending a card to the strip end").requiredOption("--id <id>", "Artifact ID").requiredOption("--screen <id>", "Screen ID").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
50370
+ const client = createAuthenticatedClient(opts.server);
50371
+ const result = await client.artifacts.attach({
50372
+ artifactID: opts.id,
50373
+ screenID: opts.screen
50374
+ });
50375
+ writeLine(
50376
+ env.stdout,
50377
+ `Artifact ${result.artifact.id} attached to screen ${result.screenID} as card ${result.cardID}.`
50378
+ );
50379
+ });
50380
+ program2.command("delete-artifact").description("Globally delete an artifact: detach from every screen, trash the bundle / forget the pointer").requiredOption("--id <id>", "Artifact ID").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
50381
+ const client = createAuthenticatedClient(opts.server);
50382
+ const result = await client.artifacts.delete({ artifactID: opts.id });
50383
+ for (const line of formatDeleteArtifactResult(result)) {
49931
50384
  writeLine(env.stdout, line);
49932
50385
  }
49933
50386
  });
50387
+ program2.command("get-artifact").description("Fetch an artifact's metadata as JSON").requiredOption("--id <id>", "Artifact ID").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
50388
+ const client = createAuthenticatedClient(opts.server);
50389
+ writeJSON(env.stdout, await client.artifacts.get({ artifactID: opts.id }));
50390
+ });
50391
+ program2.command("list-artifacts").description("List artifacts. With no filter, every artifact is returned").option("--screen <id>", "Filter to artifacts attached to this screen").option("--unplaced", "Only return artifacts attached to no screen").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
50392
+ const client = createAuthenticatedClient(opts.server);
50393
+ const filter = {};
50394
+ if (opts.screen !== void 0) filter.screenID = opts.screen;
50395
+ if (opts.unplaced) filter.unplaced = true;
50396
+ writeJSON(env.stdout, await client.artifacts.list(filter));
50397
+ });
49934
50398
  program2.command("create-screen").description("Create a new screen").requiredOption("--name <name>", "Screen name").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
49935
50399
  const client = createAuthenticatedClient(opts.server);
49936
50400
  const { screen } = await client.screens.create({ name: opts.name });
@@ -49951,6 +50415,28 @@ If you wish to display temporary content to the user, use an internal artifact i
49951
50415
  const client = createAuthenticatedClient(opts.server);
49952
50416
  writeJSON(env.stdout, await client.screens.get({ screenID: opts.id }));
49953
50417
  });
50418
+ program2.command("viewer-status").description("Print viewer state: active screen ID and connected client count").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
50419
+ const client = createAuthenticatedClient(opts.server);
50420
+ writeJSON(env.stdout, await client.viewer.state());
50421
+ });
50422
+ program2.command("set-active-screen").description("Set the screen Television's GUI is focused on; broadcast to connected clients").requiredOption("--id <id>", "Screen ID").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
50423
+ const client = createAuthenticatedClient(opts.server);
50424
+ await client.viewer.setActive({ screenID: opts.id });
50425
+ writeLine(env.stdout, `Active screen set to ${opts.id}.`);
50426
+ });
50427
+ program2.command("focus-artifact").description("Nudge connected clients to scroll an artifact into view and play a brief highlight").requiredOption("--id <id>", "Artifact ID").option("--screen <id>", "Screen ID (optional; server picks a screen the artifact is on)").option("--server <url>", "Server URL", DEFAULT_SERVER_URL).action(async (opts) => {
50428
+ const client = createAuthenticatedClient(opts.server);
50429
+ const { connectedClients } = await client.viewer.state();
50430
+ if (connectedClients === 0) {
50431
+ env.stderr.write(
50432
+ "Warning: no Television clients are connected; the focus event will be broadcast to nobody.\n"
50433
+ );
50434
+ }
50435
+ const focusInput = { artifactID: opts.id };
50436
+ if (opts.screen !== void 0) focusInput.screenID = opts.screen;
50437
+ const result = await client.viewer.focus(focusInput);
50438
+ writeLine(env.stdout, `Focused artifact ${result.artifactID} on screen ${result.screenID}.`);
50439
+ });
49954
50440
  program2.command("stop").description("Stop the Television system service").action(async () => {
49955
50441
  const daemon = env.createDaemon();
49956
50442
  await daemon.uninstall();
@@ -49979,7 +50465,7 @@ If you wish to display temporary content to the user, use an internal artifact i
49979
50465
  function listVisibleCLICommandNames() {
49980
50466
  const sink = { write: () => true };
49981
50467
  const program2 = createProgram(createEnvironment({ stdout: sink, stderr: sink }));
49982
- return program2.commands.map((command) => command.name());
50468
+ return program2.commands.filter((command) => !command._hidden).map((command) => command.name());
49983
50469
  }
49984
50470
  async function runCLI(argv, environment = {}) {
49985
50471
  const env = createEnvironment(environment);