@telepath-computer/television 0.1.46 → 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 +659 -107
- package/dist/web/assets/index-B9Lt1Vk_.js +480 -0
- package/dist/web/assets/index-CxPYSz1c.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/skill/SKILL.md +49 -10
- package/dist/web/assets/index-BlDY6iCd.css +0 -1
- package/dist/web/assets/index-Ibdvn9WQ.js +0 -474
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,8 +33778,9 @@ 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
|
-
// ../shared/src/acp-
|
|
33783
|
+
// ../shared/src/acp-types.ts
|
|
33780
33784
|
function isACPBridgeClientMessage(value) {
|
|
33781
33785
|
if (typeof value !== "object" || value === null || !("type" in value)) {
|
|
33782
33786
|
return false;
|
|
@@ -33950,21 +33954,36 @@ var ScreenClient = class {
|
|
|
33950
33954
|
};
|
|
33951
33955
|
var ArtifactClient = class {
|
|
33952
33956
|
#http;
|
|
33953
|
-
|
|
33954
|
-
constructor(http2, screens) {
|
|
33957
|
+
constructor(http2) {
|
|
33955
33958
|
this.#http = http2;
|
|
33956
|
-
this.#screens = screens;
|
|
33957
33959
|
}
|
|
33958
|
-
|
|
33959
|
-
|
|
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
|
-
"
|
|
33962
|
-
`/
|
|
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
|
-
|
|
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
|
-
|
|
34017
|
-
|
|
34018
|
-
|
|
34019
|
-
|
|
34020
|
-
|
|
34021
|
-
|
|
34022
|
-
|
|
34023
|
-
|
|
34024
|
-
|
|
34025
|
-
|
|
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
|
|
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() {
|
|
@@ -34066,6 +34121,21 @@ var import_node_path2 = __toESM(require("node:path"), 1);
|
|
|
34066
34121
|
var TRUE_ENV_VALUE = "true";
|
|
34067
34122
|
var APP_HIDDEN_DIRECTORY = ".television";
|
|
34068
34123
|
var TELEVISION_STORAGE_PATH_ENV = "TELEVISION_STORAGE_PATH";
|
|
34124
|
+
var TELEVISION_ACP_AGENT_ENV = "TELEVISION_ACP_AGENT";
|
|
34125
|
+
var ACP_AGENT_PROFILES = {
|
|
34126
|
+
openclaw: {
|
|
34127
|
+
agent: "openclaw",
|
|
34128
|
+
command: "openclaw",
|
|
34129
|
+
args: ["acp"],
|
|
34130
|
+
sessionIdStrategy: "deterministic"
|
|
34131
|
+
},
|
|
34132
|
+
hermes: {
|
|
34133
|
+
agent: "hermes",
|
|
34134
|
+
command: "hermes",
|
|
34135
|
+
args: ["acp"],
|
|
34136
|
+
sessionIdStrategy: "mapped"
|
|
34137
|
+
}
|
|
34138
|
+
};
|
|
34069
34139
|
var DEFAULT_SERVER_HOST = "localhost";
|
|
34070
34140
|
var DEFAULT_SERVER_PORT = 32848;
|
|
34071
34141
|
var DEFAULT_SERVER_URL = buildServerURL(DEFAULT_SERVER_HOST, DEFAULT_SERVER_PORT);
|
|
@@ -34075,6 +34145,16 @@ function buildServerURL(host, port) {
|
|
|
34075
34145
|
function getTelevisionStoragePath() {
|
|
34076
34146
|
return process.env[TELEVISION_STORAGE_PATH_ENV] ?? import_node_path2.default.join(import_node_os2.default.homedir(), APP_HIDDEN_DIRECTORY);
|
|
34077
34147
|
}
|
|
34148
|
+
function resolveACPAgentProfile(env = process.env) {
|
|
34149
|
+
const rawAgent = env[TELEVISION_ACP_AGENT_ENV];
|
|
34150
|
+
const agent = rawAgent === void 0 ? "openclaw" : rawAgent.trim().toLowerCase();
|
|
34151
|
+
if (agent === "openclaw" || agent === "hermes") {
|
|
34152
|
+
return ACP_AGENT_PROFILES[agent];
|
|
34153
|
+
}
|
|
34154
|
+
throw new Error(
|
|
34155
|
+
`Unsupported TELEVISION_ACP_AGENT "${rawAgent}". Supported values: openclaw, hermes.`
|
|
34156
|
+
);
|
|
34157
|
+
}
|
|
34078
34158
|
function isVitestRuntime() {
|
|
34079
34159
|
return process.env.VITEST === TRUE_ENV_VALUE;
|
|
34080
34160
|
}
|
|
@@ -47959,14 +48039,24 @@ var createArtifactSchema = external_exports.object({
|
|
|
47959
48039
|
title: external_exports.string(),
|
|
47960
48040
|
externalFilePath: external_exports.string().optional()
|
|
47961
48041
|
}).strict();
|
|
48042
|
+
var createArtifactBodySchema = createArtifactSchema.extend({
|
|
48043
|
+
screenID: external_exports.string().optional()
|
|
48044
|
+
}).strict();
|
|
47962
48045
|
var patchArtifactSchema = external_exports.object({
|
|
47963
48046
|
title: external_exports.string().optional()
|
|
47964
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;
|
|
47965
48054
|
var HTTP_CREATED = 201;
|
|
47966
48055
|
var HTTP_NO_CONTENT = 204;
|
|
47967
48056
|
var HTTP_BAD_REQUEST = 400;
|
|
47968
48057
|
var HTTP_FORBIDDEN = 403;
|
|
47969
48058
|
var HTTP_NOT_FOUND = 404;
|
|
48059
|
+
var HTTP_CONFLICT = 409;
|
|
47970
48060
|
var CORS_HEADERS = {
|
|
47971
48061
|
"Access-Control-Allow-Origin": "*",
|
|
47972
48062
|
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
|
|
@@ -47993,19 +48083,29 @@ function handleStoreError(res, error48, fallback) {
|
|
|
47993
48083
|
sendError(res, HTTP_BAD_REQUEST, error48.message);
|
|
47994
48084
|
return;
|
|
47995
48085
|
}
|
|
48086
|
+
if (error48 instanceof ConflictError) {
|
|
48087
|
+
sendError(res, HTTP_CONFLICT, error48.message);
|
|
48088
|
+
return;
|
|
48089
|
+
}
|
|
47996
48090
|
throw error48 instanceof Error ? error48 : new Error(fallback);
|
|
47997
48091
|
}
|
|
47998
48092
|
function registerRoutes(app, store, options) {
|
|
48093
|
+
const getConnectedClientCount = options.getConnectedClientCount ?? (() => 0);
|
|
47999
48094
|
const auth = options.requireAuth ? [options.requireAuth] : [];
|
|
48000
48095
|
app.use("/screens", applyCorsMiddleware);
|
|
48001
48096
|
app.use("/artifacts", applyCorsMiddleware);
|
|
48002
48097
|
app.use("/views", applyCorsMiddleware);
|
|
48098
|
+
app.use("/viewer", applyCorsMiddleware);
|
|
48003
48099
|
app.options("/screens", (_req, res) => res.status(HTTP_NO_CONTENT).end());
|
|
48004
48100
|
app.options("/screens/:id", (_req, res) => res.status(HTTP_NO_CONTENT).end());
|
|
48005
48101
|
app.options("/screens/:id/artifact", (_req, res) => res.status(HTTP_NO_CONTENT).end());
|
|
48006
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());
|
|
48007
48104
|
app.options("/artifacts/:id/*", (_req, res) => res.status(HTTP_NO_CONTENT).end());
|
|
48008
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());
|
|
48009
48109
|
app.options("/views", (_req, res) => res.status(HTTP_NO_CONTENT).end());
|
|
48010
48110
|
app.options("/views/:id", (_req, res) => res.status(HTTP_NO_CONTENT).end());
|
|
48011
48111
|
app.options("/views/:id/*", (_req, res) => res.status(HTTP_NO_CONTENT).end());
|
|
@@ -48066,6 +48166,21 @@ function registerRoutes(app, store, options) {
|
|
|
48066
48166
|
handleStoreError(res, error48, "Failed to update screen");
|
|
48067
48167
|
}
|
|
48068
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
|
+
);
|
|
48069
48184
|
app.post("/screens/:id/artifact", ...auth, (req, res) => {
|
|
48070
48185
|
const parsed = createArtifactSchema.safeParse(req.body);
|
|
48071
48186
|
if (!parsed.success) {
|
|
@@ -48081,27 +48196,96 @@ function registerRoutes(app, store, options) {
|
|
|
48081
48196
|
});
|
|
48082
48197
|
res.status(HTTP_CREATED).json({
|
|
48083
48198
|
artifact,
|
|
48084
|
-
// Internal artifacts return the pending bundle path so the caller can
|
|
48085
|
-
// write files into it before commit. External artifacts don't have a
|
|
48086
|
-
// pending bundle.
|
|
48087
48199
|
...artifact.externalFilePath ? {} : { pendingPath: getArtifactPendingBundlePath(store.storagePath, artifact.id) }
|
|
48088
48200
|
});
|
|
48089
48201
|
} catch (error48) {
|
|
48090
48202
|
handleStoreError(res, error48, "Failed to create artifact");
|
|
48091
48203
|
}
|
|
48092
48204
|
});
|
|
48093
|
-
app.
|
|
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(
|
|
48094
48278
|
"/screens/:screenID/artifacts/:artifactID",
|
|
48095
48279
|
...auth,
|
|
48096
48280
|
(req, res) => {
|
|
48097
48281
|
try {
|
|
48098
|
-
const result = store.
|
|
48099
|
-
req.params.
|
|
48100
|
-
req.params.
|
|
48101
|
-
);
|
|
48282
|
+
const result = store.attachArtifact({
|
|
48283
|
+
screenID: req.params.screenID,
|
|
48284
|
+
artifactID: req.params.artifactID
|
|
48285
|
+
});
|
|
48102
48286
|
res.json(result);
|
|
48103
48287
|
} catch (error48) {
|
|
48104
|
-
handleStoreError(res, error48, "Failed to
|
|
48288
|
+
handleStoreError(res, error48, "Failed to attach artifact");
|
|
48105
48289
|
}
|
|
48106
48290
|
}
|
|
48107
48291
|
);
|
|
@@ -48213,6 +48397,41 @@ function registerRoutes(app, store, options) {
|
|
|
48213
48397
|
app.put("/artifacts/:id/content/*", ...auth, (_req, res) => {
|
|
48214
48398
|
sendError(res, HTTP_FORBIDDEN, "Only the primary content file is HTTP-writable");
|
|
48215
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
|
+
});
|
|
48216
48435
|
const viewStaticHandlers = /* @__PURE__ */ new Map();
|
|
48217
48436
|
const getViewStaticHandler = (id, viewDir) => {
|
|
48218
48437
|
let handler = viewStaticHandlers.get(id);
|
|
@@ -48343,6 +48562,16 @@ var EventStreamServer = class extends withDisposable(class {
|
|
|
48343
48562
|
});
|
|
48344
48563
|
this.subscribeStore();
|
|
48345
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
|
+
}
|
|
48346
48575
|
handleUpgrade(request, socket, head) {
|
|
48347
48576
|
this.wsServer.handleUpgrade(request, socket, head, (ws) => {
|
|
48348
48577
|
this.wsServer.emit("connection", ws, request);
|
|
@@ -48365,7 +48594,10 @@ var EventStreamServer = class extends withDisposable(class {
|
|
|
48365
48594
|
}
|
|
48366
48595
|
subscribeStore() {
|
|
48367
48596
|
const onArtifactCreated = (event) => {
|
|
48368
|
-
this.broadcast({ type: "artifact-created",
|
|
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 });
|
|
48369
48601
|
};
|
|
48370
48602
|
const onArtifactUpdated = (event) => {
|
|
48371
48603
|
this.broadcast({ type: "artifact-updated", artifact: event.artifact });
|
|
@@ -48385,21 +48617,33 @@ var EventStreamServer = class extends withDisposable(class {
|
|
|
48385
48617
|
const onScreenRemoved = (event) => {
|
|
48386
48618
|
this.broadcast({ type: "screen-removed", screenID: event.screenID });
|
|
48387
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
|
+
};
|
|
48388
48626
|
this.store.addEventListener("artifact-created", onArtifactCreated);
|
|
48627
|
+
this.store.addEventListener("artifact-attached", onArtifactAttached);
|
|
48389
48628
|
this.store.addEventListener("artifact-updated", onArtifactUpdated);
|
|
48390
48629
|
this.store.addEventListener("artifact-removed", onArtifactRemoved);
|
|
48391
48630
|
this.store.addEventListener("artifact-content-changed", onArtifactContentChanged);
|
|
48392
48631
|
this.store.addEventListener("screen-created", onScreenCreated);
|
|
48393
48632
|
this.store.addEventListener("screen-updated", onScreenUpdated);
|
|
48394
48633
|
this.store.addEventListener("screen-removed", onScreenRemoved);
|
|
48634
|
+
this.store.addEventListener("viewer-active-screen-changed", onViewerActiveScreenChanged);
|
|
48635
|
+
this.store.addEventListener("viewer-focus", onViewerFocus);
|
|
48395
48636
|
this.unsubscribe.push(
|
|
48396
48637
|
() => this.store.removeEventListener("artifact-created", onArtifactCreated),
|
|
48638
|
+
() => this.store.removeEventListener("artifact-attached", onArtifactAttached),
|
|
48397
48639
|
() => this.store.removeEventListener("artifact-updated", onArtifactUpdated),
|
|
48398
48640
|
() => this.store.removeEventListener("artifact-removed", onArtifactRemoved),
|
|
48399
48641
|
() => this.store.removeEventListener("artifact-content-changed", onArtifactContentChanged),
|
|
48400
48642
|
() => this.store.removeEventListener("screen-created", onScreenCreated),
|
|
48401
48643
|
() => this.store.removeEventListener("screen-updated", onScreenUpdated),
|
|
48402
|
-
() => 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)
|
|
48403
48647
|
);
|
|
48404
48648
|
}
|
|
48405
48649
|
broadcast(event) {
|
|
@@ -48417,8 +48661,26 @@ var import_node_child_process2 = require("node:child_process");
|
|
|
48417
48661
|
var import_promises2 = require("node:fs/promises");
|
|
48418
48662
|
var import_node_os3 = require("node:os");
|
|
48419
48663
|
var import_node_path5 = __toESM(require("node:path"), 1);
|
|
48420
|
-
var
|
|
48421
|
-
|
|
48664
|
+
var launchACPProcess = (command, args, options) => {
|
|
48665
|
+
const child = (0, import_node_child_process2.spawn)(command, args, options);
|
|
48666
|
+
const directKill = child.kill.bind(child);
|
|
48667
|
+
child.kill = (signal) => {
|
|
48668
|
+
const pid = child.pid;
|
|
48669
|
+
const sig = signal ?? "SIGTERM";
|
|
48670
|
+
if (pid != null) {
|
|
48671
|
+
try {
|
|
48672
|
+
process.kill(-pid, sig);
|
|
48673
|
+
return true;
|
|
48674
|
+
} catch (error48) {
|
|
48675
|
+
if (error48.code === "ESRCH") {
|
|
48676
|
+
return true;
|
|
48677
|
+
}
|
|
48678
|
+
}
|
|
48679
|
+
}
|
|
48680
|
+
return directKill(sig);
|
|
48681
|
+
};
|
|
48682
|
+
return child;
|
|
48683
|
+
};
|
|
48422
48684
|
var ACP_STOP_TIMEOUT_MS = 5e3;
|
|
48423
48685
|
var ACP_STUB_CWD = import_node_path5.default.join((0, import_node_os3.homedir)(), ".television", "acp");
|
|
48424
48686
|
var ACPBridge = class extends withDisposable(class {
|
|
@@ -48429,9 +48691,15 @@ var ACPBridge = class extends withDisposable(class {
|
|
|
48429
48691
|
stdoutBuffer = "";
|
|
48430
48692
|
stderrBuffer = "";
|
|
48431
48693
|
send;
|
|
48432
|
-
|
|
48694
|
+
profile;
|
|
48695
|
+
launchProcess;
|
|
48696
|
+
sessionCwd;
|
|
48697
|
+
constructor(options) {
|
|
48433
48698
|
super();
|
|
48434
|
-
this.send = send;
|
|
48699
|
+
this.send = options.send;
|
|
48700
|
+
this.profile = options.profile ?? resolveACPAgentProfile();
|
|
48701
|
+
this.launchProcess = options.launchProcess ?? launchACPProcess;
|
|
48702
|
+
this.sessionCwd = import_node_path5.default.resolve(process.cwd());
|
|
48435
48703
|
}
|
|
48436
48704
|
handleClientMessage(message) {
|
|
48437
48705
|
switch (message.type) {
|
|
@@ -48478,9 +48746,10 @@ var ACPBridge = class extends withDisposable(class {
|
|
|
48478
48746
|
this.error = null;
|
|
48479
48747
|
this.sendStatus();
|
|
48480
48748
|
await (0, import_promises2.mkdir)(ACP_STUB_CWD, { recursive: true });
|
|
48481
|
-
const child = (
|
|
48749
|
+
const child = this.launchProcess(this.profile.command, this.profile.args, {
|
|
48482
48750
|
cwd: ACP_STUB_CWD,
|
|
48483
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
48751
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
48752
|
+
detached: true
|
|
48484
48753
|
});
|
|
48485
48754
|
this.child = child;
|
|
48486
48755
|
child.once("spawn", () => {
|
|
@@ -48529,17 +48798,28 @@ var ACPBridge = class extends withDisposable(class {
|
|
|
48529
48798
|
if (!line) {
|
|
48530
48799
|
continue;
|
|
48531
48800
|
}
|
|
48801
|
+
let message;
|
|
48532
48802
|
try {
|
|
48533
|
-
|
|
48534
|
-
this.send({ type: "acp-bridge-message", message });
|
|
48803
|
+
message = JSON.parse(line);
|
|
48535
48804
|
} catch (error48) {
|
|
48536
|
-
|
|
48537
|
-
|
|
48538
|
-
|
|
48805
|
+
const reason = error48 instanceof Error ? error48.message : String(error48);
|
|
48806
|
+
console.error(`ACPBridge: dropping non-JSON stdout line (${reason}): ${line}`);
|
|
48807
|
+
continue;
|
|
48539
48808
|
}
|
|
48809
|
+
this.send({ type: "acp-bridge-message", message });
|
|
48540
48810
|
}
|
|
48541
48811
|
}
|
|
48542
48812
|
sendStatus() {
|
|
48813
|
+
if (this.status === "ready") {
|
|
48814
|
+
this.send({
|
|
48815
|
+
type: "acp-bridge-status",
|
|
48816
|
+
status: "ready",
|
|
48817
|
+
agent: this.profile.agent,
|
|
48818
|
+
sessionIdStrategy: this.profile.sessionIdStrategy,
|
|
48819
|
+
sessionCwd: this.sessionCwd
|
|
48820
|
+
});
|
|
48821
|
+
return;
|
|
48822
|
+
}
|
|
48543
48823
|
const message = {
|
|
48544
48824
|
type: "acp-bridge-status",
|
|
48545
48825
|
status: this.status,
|
|
@@ -48560,19 +48840,24 @@ var ACPServer = class extends withDisposable(class {
|
|
|
48560
48840
|
bridges = /* @__PURE__ */ new Map();
|
|
48561
48841
|
authToken;
|
|
48562
48842
|
publicServer;
|
|
48843
|
+
profile;
|
|
48563
48844
|
constructor(options) {
|
|
48564
48845
|
super();
|
|
48565
48846
|
this.authToken = options.authToken;
|
|
48566
48847
|
this.publicServer = options.publicServer;
|
|
48848
|
+
this.profile = resolveACPAgentProfile();
|
|
48567
48849
|
this.wsServer = new import_websocket_server.default({ noServer: true });
|
|
48568
48850
|
this.wsServer.on("connection", (socket, request) => {
|
|
48569
48851
|
if (!this.publicServer && !isAuthorizedQueryToken(request.url, this.authToken)) {
|
|
48570
48852
|
socket.close(AUTH_FAILED_CLOSE_CODE2, "Authentication failed");
|
|
48571
48853
|
return;
|
|
48572
48854
|
}
|
|
48573
|
-
const bridge = new ACPBridge(
|
|
48574
|
-
|
|
48575
|
-
|
|
48855
|
+
const bridge = new ACPBridge({
|
|
48856
|
+
profile: this.profile,
|
|
48857
|
+
send: (message) => {
|
|
48858
|
+
if (socket.readyState === import_websocket.default.OPEN) {
|
|
48859
|
+
socket.send(JSON.stringify(message));
|
|
48860
|
+
}
|
|
48576
48861
|
}
|
|
48577
48862
|
});
|
|
48578
48863
|
this.bridges.set(socket, bridge);
|
|
@@ -48641,13 +48926,14 @@ var Server = class {
|
|
|
48641
48926
|
res.json({ status: "ok" });
|
|
48642
48927
|
});
|
|
48643
48928
|
this.app.use(import_express2.default.json());
|
|
48929
|
+
this.events = new EventStreamServer({ store: this.store, publicServer: this.publicServer });
|
|
48644
48930
|
registerRoutes(this.app, this.store, {
|
|
48645
|
-
requireAuth: this.publicServer ? null : this.requireAuthorization
|
|
48931
|
+
requireAuth: this.publicServer ? null : this.requireAuthorization,
|
|
48932
|
+
getConnectedClientCount: () => this.events.getConnectedClientCount()
|
|
48646
48933
|
});
|
|
48647
48934
|
if (options.staticDir) {
|
|
48648
48935
|
this.app.use(import_express2.default.static(options.staticDir));
|
|
48649
48936
|
}
|
|
48650
|
-
this.events = new EventStreamServer({ store: this.store, publicServer: this.publicServer });
|
|
48651
48937
|
this.acp = new ACPServer({ authToken: this.store.authToken, publicServer: this.publicServer });
|
|
48652
48938
|
this.httpServer.on("upgrade", (request, socket, head) => {
|
|
48653
48939
|
const parsed = new URL(request.url ?? "/", this.baseURL);
|
|
@@ -48739,6 +49025,7 @@ var ServerStore = class extends EventTarget {
|
|
|
48739
49025
|
storagePath;
|
|
48740
49026
|
authToken;
|
|
48741
49027
|
screens = /* @__PURE__ */ new Map();
|
|
49028
|
+
activeScreenID = null;
|
|
48742
49029
|
_artifacts = /* @__PURE__ */ new Map();
|
|
48743
49030
|
contentWatchers = /* @__PURE__ */ new Map();
|
|
48744
49031
|
contentWatchDebounceTimers = /* @__PURE__ */ new Map();
|
|
@@ -48754,6 +49041,7 @@ var ServerStore = class extends EventTarget {
|
|
|
48754
49041
|
this.views = this.scanViewRegistry();
|
|
48755
49042
|
this.authToken = this.loadOrCreateAuthToken();
|
|
48756
49043
|
this.load();
|
|
49044
|
+
this.loadViewerState();
|
|
48757
49045
|
this.startContentWatchersForLoadedArtifacts();
|
|
48758
49046
|
if (this.screens.size === 0) {
|
|
48759
49047
|
this.createDefaultScreen();
|
|
@@ -48874,8 +49162,15 @@ var ServerStore = class extends EventTarget {
|
|
|
48874
49162
|
listArtifacts() {
|
|
48875
49163
|
return [...this._artifacts.values()];
|
|
48876
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
|
+
*/
|
|
48877
49172
|
createArtifact(input) {
|
|
48878
|
-
const screen = this.requireScreen(input.screenID);
|
|
49173
|
+
const screen = input.screenID !== void 0 ? this.requireScreen(input.screenID) : null;
|
|
48879
49174
|
const artifact = createArtifact({
|
|
48880
49175
|
type: input.type,
|
|
48881
49176
|
title: input.title,
|
|
@@ -48890,14 +49185,19 @@ var ServerStore = class extends EventTarget {
|
|
|
48890
49185
|
if (artifact.status === "committed") {
|
|
48891
49186
|
this.startWatchingArtifactContent(artifact.id);
|
|
48892
49187
|
}
|
|
48893
|
-
|
|
48894
|
-
|
|
48895
|
-
|
|
48896
|
-
|
|
48897
|
-
|
|
48898
|
-
|
|
48899
|
-
|
|
48900
|
-
|
|
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
|
+
}
|
|
48901
49201
|
return artifact;
|
|
48902
49202
|
}
|
|
48903
49203
|
updateArtifact(input) {
|
|
@@ -48983,18 +49283,93 @@ var ServerStore = class extends EventTarget {
|
|
|
48983
49283
|
this.dispatchEvent(new ArtifactUpdatedEvent("artifact-updated", { artifact }));
|
|
48984
49284
|
}
|
|
48985
49285
|
/**
|
|
48986
|
-
*
|
|
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.
|
|
48987
49289
|
*
|
|
48988
|
-
*
|
|
48989
|
-
|
|
48990
|
-
|
|
48991
|
-
|
|
48992
|
-
|
|
48993
|
-
|
|
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.
|
|
48994
49322
|
*
|
|
48995
49323
|
* Throws `NotFoundError` when the screen does not exist, the artifact
|
|
48996
49324
|
* does not exist, or the artifact is not a member of the screen.
|
|
48997
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
|
+
*/
|
|
48998
49373
|
removeArtifactFromScreen(artifactID, screenID) {
|
|
48999
49374
|
const screen = this.requireScreen(screenID);
|
|
49000
49375
|
const artifact = this._artifacts.get(artifactID);
|
|
@@ -49009,11 +49384,7 @@ var ServerStore = class extends EventTarget {
|
|
|
49009
49384
|
}
|
|
49010
49385
|
const referencingScreenIDs = this.collectReferencingScreenIDs(artifactID);
|
|
49011
49386
|
if (referencingScreenIDs.some((id) => id !== screenID)) {
|
|
49012
|
-
|
|
49013
|
-
this.persistScreen(screen);
|
|
49014
|
-
this.dispatchEvent(
|
|
49015
|
-
new ArtifactRemovedEvent("artifact-removed", { artifactID, screenID })
|
|
49016
|
-
);
|
|
49387
|
+
this.detachArtifact({ screenID, artifactID });
|
|
49017
49388
|
return { outcome: "unlinked", artifactID };
|
|
49018
49389
|
}
|
|
49019
49390
|
return this.cascadeArtifactRemoval(artifactID);
|
|
@@ -49037,6 +49408,79 @@ var ServerStore = class extends EventTarget {
|
|
|
49037
49408
|
this.dispatchEvent(new ScreenUpdatedEvent("screen-updated", { screen }));
|
|
49038
49409
|
return screen;
|
|
49039
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
|
+
}
|
|
49040
49484
|
removeScreen(screenID) {
|
|
49041
49485
|
const screen = this.requireScreen(screenID);
|
|
49042
49486
|
const snapshot = { ...screen, layout: structuredClone(screen.layout) };
|
|
@@ -49321,6 +49765,35 @@ var ServerStore = class extends EventTarget {
|
|
|
49321
49765
|
registry2.sort((a, b) => a.id.localeCompare(b.id));
|
|
49322
49766
|
return registry2;
|
|
49323
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
|
+
}
|
|
49324
49797
|
loadOrCreateAuthToken() {
|
|
49325
49798
|
if ((0, import_node_fs3.existsSync)(this.tokenPath)) {
|
|
49326
49799
|
return (0, import_node_fs3.readFileSync)(this.tokenPath, "utf8").trim();
|
|
@@ -49485,6 +49958,30 @@ Review docs/storage.md for the current schema, and either update the file to mat
|
|
|
49485
49958
|
return import_node_path6.default.join(this.storagePath, "token");
|
|
49486
49959
|
}
|
|
49487
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
|
+
}
|
|
49488
49985
|
|
|
49489
49986
|
// src/index.ts
|
|
49490
49987
|
var import_meta = {};
|
|
@@ -49513,8 +50010,8 @@ function getDevPackageDir() {
|
|
|
49513
50010
|
return import_node_path7.default.resolve(import_node_path7.default.dirname((0, import_node_url.fileURLToPath)(import_meta.url)), "..");
|
|
49514
50011
|
}
|
|
49515
50012
|
function readCLIVersion() {
|
|
49516
|
-
if ("0.1.
|
|
49517
|
-
return "0.1.
|
|
50013
|
+
if ("0.1.48".length > 0) {
|
|
50014
|
+
return "0.1.48";
|
|
49518
50015
|
}
|
|
49519
50016
|
const devPackageJsonPath = import_node_path7.default.join(getDevPackageDir(), "package.json");
|
|
49520
50017
|
if (!(0, import_node_fs4.existsSync)(devPackageJsonPath)) {
|
|
@@ -49523,8 +50020,8 @@ function readCLIVersion() {
|
|
|
49523
50020
|
return JSON.parse((0, import_node_fs4.readFileSync)(devPackageJsonPath, "utf8")).version;
|
|
49524
50021
|
}
|
|
49525
50022
|
function readWorkflowHelpText() {
|
|
49526
|
-
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) {
|
|
49527
|
-
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';
|
|
49528
50025
|
}
|
|
49529
50026
|
const devSkillPath = import_node_path7.default.join(getDevPackageDir(), "skill/SKILL.md");
|
|
49530
50027
|
if (!(0, import_node_fs4.existsSync)(devSkillPath)) {
|
|
@@ -49601,31 +50098,7 @@ function validateExternalArtifactPath(inputPath) {
|
|
|
49601
50098
|
}
|
|
49602
50099
|
return inputPath;
|
|
49603
50100
|
}
|
|
49604
|
-
function formatArtifactRemovalResult(result
|
|
49605
|
-
if (context.kind === "top-level") {
|
|
49606
|
-
const sID = context.screenID;
|
|
49607
|
-
if (result.outcome === "unlinked") {
|
|
49608
|
-
return [`Artifact ${result.artifactID} unlinked from screen ${sID} (still referenced elsewhere).`];
|
|
49609
|
-
}
|
|
49610
|
-
if (result.outcome === "discarded") {
|
|
49611
|
-
return [
|
|
49612
|
-
`Pending-create artifact ${result.artifactID} unlinked from screen ${sID} and discarded.`,
|
|
49613
|
-
"No trash paths were written because no committed artifact existed yet."
|
|
49614
|
-
];
|
|
49615
|
-
}
|
|
49616
|
-
if ("bundlePath" in result) {
|
|
49617
|
-
return [
|
|
49618
|
-
`Internal artifact ${result.artifactID} unlinked from screen ${sID}; last reference, moved to trash:`,
|
|
49619
|
-
` metadata: ${result.metadataPath}`,
|
|
49620
|
-
` bundle: ${result.bundlePath}`
|
|
49621
|
-
];
|
|
49622
|
-
}
|
|
49623
|
-
return [
|
|
49624
|
-
`External artifact ${result.artifactID} unlinked from screen ${sID}; last reference, moved to trash:`,
|
|
49625
|
-
` metadata: ${result.metadataPath}`,
|
|
49626
|
-
` external file ${result.externalFilePath} not touched.`
|
|
49627
|
-
];
|
|
49628
|
-
}
|
|
50101
|
+
function formatArtifactRemovalResult(result) {
|
|
49629
50102
|
if (result.outcome === "unlinked") {
|
|
49630
50103
|
return [` ${result.artifactID}: unlinked (still referenced elsewhere)`];
|
|
49631
50104
|
}
|
|
@@ -49645,6 +50118,26 @@ function formatArtifactRemovalResult(result, context) {
|
|
|
49645
50118
|
` external file ${result.externalFilePath} not touched.`
|
|
49646
50119
|
];
|
|
49647
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
|
+
}
|
|
49648
50141
|
function formatScreenRemovalResult(result) {
|
|
49649
50142
|
const header = [
|
|
49650
50143
|
`Screen ${result.screenID} moved to trash:`,
|
|
@@ -49656,7 +50149,7 @@ function formatScreenRemovalResult(result) {
|
|
|
49656
50149
|
}
|
|
49657
50150
|
header.push(`${result.artifactResults.length} referenced artifact(s):`);
|
|
49658
50151
|
for (const artifactResult of result.artifactResults) {
|
|
49659
|
-
header.push(...formatArtifactRemovalResult(artifactResult
|
|
50152
|
+
header.push(...formatArtifactRemovalResult(artifactResult));
|
|
49660
50153
|
}
|
|
49661
50154
|
return header;
|
|
49662
50155
|
}
|
|
@@ -49858,13 +50351,50 @@ If you wish to display temporary content to the user, use an internal artifact i
|
|
|
49858
50351
|
await client.artifacts.update({ artifactID: opts.id, title: opts.title });
|
|
49859
50352
|
writeLine(env.stdout, `Artifact ${opts.id} updated.`);
|
|
49860
50353
|
});
|
|
49861
|
-
|
|
50354
|
+
const detachAction = async (opts) => {
|
|
49862
50355
|
const client = createAuthenticatedClient(opts.server);
|
|
49863
|
-
const result = await client.artifacts.
|
|
49864
|
-
|
|
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)) {
|
|
49865
50384
|
writeLine(env.stdout, line);
|
|
49866
50385
|
}
|
|
49867
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
|
+
});
|
|
49868
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) => {
|
|
49869
50399
|
const client = createAuthenticatedClient(opts.server);
|
|
49870
50400
|
const { screen } = await client.screens.create({ name: opts.name });
|
|
@@ -49885,6 +50415,28 @@ If you wish to display temporary content to the user, use an internal artifact i
|
|
|
49885
50415
|
const client = createAuthenticatedClient(opts.server);
|
|
49886
50416
|
writeJSON(env.stdout, await client.screens.get({ screenID: opts.id }));
|
|
49887
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
|
+
});
|
|
49888
50440
|
program2.command("stop").description("Stop the Television system service").action(async () => {
|
|
49889
50441
|
const daemon = env.createDaemon();
|
|
49890
50442
|
await daemon.uninstall();
|
|
@@ -49913,7 +50465,7 @@ If you wish to display temporary content to the user, use an internal artifact i
|
|
|
49913
50465
|
function listVisibleCLICommandNames() {
|
|
49914
50466
|
const sink = { write: () => true };
|
|
49915
50467
|
const program2 = createProgram(createEnvironment({ stdout: sink, stderr: sink }));
|
|
49916
|
-
return program2.commands.map((command) => command.name());
|
|
50468
|
+
return program2.commands.filter((command) => !command._hidden).map((command) => command.name());
|
|
49917
50469
|
}
|
|
49918
50470
|
async function runCLI(argv, environment = {}) {
|
|
49919
50471
|
const env = createEnvironment(environment);
|