@buckeyestudio/pi-wire 18.4.13

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.
@@ -0,0 +1,323 @@
1
+ /**
2
+ * Wire contract for Skillshare (`skills.omp.sh`): an npm-style registry for
3
+ * omp skills. Shared by the omp CLI (`omp skill …`), the Go server
4
+ * (`stencil/apps/skills`), and its web UI (which mirrors this file).
5
+ *
6
+ * Packages are scoped: `@scope/name`. A scope is a Stencil username claimed on
7
+ * first publish and bound to the account's immutable `sub` forever (usernames
8
+ * can be renamed and recycled; scopes cannot). `name` is the SKILL.md
9
+ * frontmatter `name`. Published versions are immutable.
10
+ *
11
+ * Registry fields ride in SKILL.md `metadata` (the Agent Skills frontmatter is
12
+ * closed to name/description/license/compatibility/metadata/allowed-tools):
13
+ * `metadata.version` (semver, required to publish), `metadata.keywords`
14
+ * (comma-separated), `metadata.repository`, `metadata.homepage`.
15
+ */
16
+
17
+ /** Default registry; `skills.registryUrl` overrides it. */
18
+ export const DEFAULT_SKILLS_URL = "https://skills.omp.sh";
19
+
20
+ /** Scopes are Stencil usernames (or registry orgs): lowercase letters, digits, underscores; 3–32 chars. */
21
+ export const SKILL_SCOPE_RE = /^[a-z0-9][a-z0-9_]{2,31}$/;
22
+ /** Registry skill names: ASCII kebab-case, 1–64 chars, no leading/trailing/double hyphens. */
23
+ export const SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
24
+ export const SKILL_NAME_MAX = 64;
25
+ /** `@scope/name`, optionally `@version-or-range-or-tag`. */
26
+ export const SKILL_SPEC_RE = /^@([a-z0-9][a-z0-9_]{2,31})\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:@(.+))?$/;
27
+
28
+ /** Publish limits, enforced by both the CLI and the server. */
29
+ export const SKILL_LIMITS = {
30
+ /** Compressed `.tgz` upload. */
31
+ tarballBytes: 5 * 1024 * 1024,
32
+ /** Sum of all file sizes after unpacking. */
33
+ unpackedBytes: 20 * 1024 * 1024,
34
+ files: 1000,
35
+ /** Longest path inside the package. */
36
+ pathLength: 255,
37
+ descriptionLength: 1024,
38
+ keywords: 20,
39
+ keywordLength: 50,
40
+ deprecationLength: 500,
41
+ } as const;
42
+
43
+ /** Paths excluded from packs in addition to `.skillignore` entries. */
44
+ export const SKILL_DEFAULT_IGNORES = [
45
+ ".git",
46
+ ".DS_Store",
47
+ "node_modules",
48
+ "__pycache__",
49
+ "*.pyc",
50
+ "evals",
51
+ ".skillignore",
52
+ ] as const;
53
+
54
+ /**
55
+ * Publish request: `PUT SKILLS_ROUTES.publish(scope, name, version)`, body is
56
+ * the `.tgz` (gzip tar; entries are paths relative to the skill root, regular
57
+ * files only, sorted, mtime 0, mode 0644 or 0755). Auth: `Authorization:
58
+ * Bearer <Stencil access token | registry publish token>`.
59
+ */
60
+ export const SKILL_HEADERS = {
61
+ /** Required: SRI `sha512-<base64>` of the body. */
62
+ integrity: "X-Skill-Integrity",
63
+ /** Optional dist-tag to point at the new version (default `latest` for stable, none for prereleases). */
64
+ tag: "X-Skill-Tag",
65
+ /** Optional base64(JSON {@link SkillReportedProvenance}); shown as "reported". */
66
+ provenance: "X-Skill-Provenance",
67
+ /** Optional GitHub Actions OIDC token (audience {@link SKILLS_OIDC_AUDIENCE}); verified server-side. */
68
+ githubOidc: "X-Skill-GitHub-OIDC",
69
+ } as const;
70
+
71
+ /** Audience CI must request for GitHub Actions OIDC tokens. */
72
+ export const SKILLS_OIDC_AUDIENCE = "skills.omp.sh";
73
+
74
+ /** Env var holding a registry publish token for CI (`sks_…`); wins over the Stencil login. */
75
+ export const SKILLS_TOKEN_ENV = "SKILLS_TOKEN";
76
+
77
+ /** Public identity of an account: current username + avatar seed (hash of the immutable sub). */
78
+ export interface SkillUser {
79
+ username: string;
80
+ /** Hex seed for the generated dither avatar; never the raw sub. */
81
+ avatar: string;
82
+ }
83
+
84
+ export interface SkillReportedProvenance {
85
+ ompVersion: string;
86
+ gitRemote?: string;
87
+ gitCommit?: string;
88
+ }
89
+
90
+ /** Provenance proven by a verified GitHub Actions OIDC token. */
91
+ export interface SkillVerifiedProvenance {
92
+ kind: "github-actions";
93
+ repository: string;
94
+ workflow: string;
95
+ ref: string;
96
+ sha: string;
97
+ runUrl: string;
98
+ }
99
+
100
+ export interface SkillProvenance {
101
+ publisher: SkillUser;
102
+ /** Publish came from a registry token rather than an interactive login. */
103
+ viaToken: boolean;
104
+ reported?: SkillReportedProvenance;
105
+ verified?: SkillVerifiedProvenance;
106
+ }
107
+
108
+ export interface SkillFile {
109
+ /** POSIX path relative to the skill root. */
110
+ path: string;
111
+ size: number;
112
+ /** Hex SHA-256 of the content. */
113
+ sha256: string;
114
+ executable: boolean;
115
+ }
116
+
117
+ /** One version as listed in a packument. */
118
+ export interface SkillVersionSummary {
119
+ version: string;
120
+ publishedAt: number;
121
+ publisher: SkillUser;
122
+ integrity: string;
123
+ size: number;
124
+ unpackedSize: number;
125
+ fileCount: number;
126
+ /** Ships executables or anything under `scripts/`. */
127
+ hasScripts: boolean;
128
+ yanked: boolean;
129
+ deprecated?: string;
130
+ }
131
+
132
+ /** Daily download counts, oldest first: `[YYYY-MM-DD, count]`. */
133
+ export type SkillDownloadSeries = [day: string, count: number][];
134
+
135
+ /** `GET SKILLS_ROUTES.packument`: everything about a package. */
136
+ export interface SkillPackument {
137
+ scope: string;
138
+ name: string;
139
+ /** From the `latest` version. */
140
+ description: string;
141
+ keywords: string[];
142
+ license?: string;
143
+ repository?: string;
144
+ homepage?: string;
145
+ owners: SkillUser[];
146
+ /** e.g. `{ latest: "1.2.0", next: "2.0.0-beta.1" }`. */
147
+ distTags: Record<string, string>;
148
+ versions: Record<string, SkillVersionSummary>;
149
+ createdAt: number;
150
+ updatedAt: number;
151
+ downloads: { weekly: number; total: number; daily: SkillDownloadSeries };
152
+ /** Signed-in viewer may publish, tag, yank, deprecate, and manage owners. */
153
+ canManage: boolean;
154
+ }
155
+
156
+ /** `GET SKILLS_ROUTES.version`: one immutable version. `:version` may be a dist-tag. */
157
+ export interface SkillVersionManifest extends SkillVersionSummary {
158
+ scope: string;
159
+ name: string;
160
+ description: string;
161
+ license?: string;
162
+ compatibility?: string;
163
+ allowedTools?: string;
164
+ /** SKILL.md `metadata` verbatim. */
165
+ metadata: Record<string, string>;
166
+ keywords: string[];
167
+ repository?: string;
168
+ homepage?: string;
169
+ files: SkillFile[];
170
+ /** Package path rendered as the README (`README.md` if present, else `SKILL.md`). */
171
+ readmePath: string;
172
+ provenance: SkillProvenance;
173
+ }
174
+
175
+ /** `GET SKILLS_ROUTES.readme`: sanitized HTML rendered at publish time. */
176
+ export interface SkillReadme {
177
+ html: string;
178
+ }
179
+
180
+ /** `GET SKILLS_ROUTES.source`: one file for the code viewer. */
181
+ export interface SkillSource {
182
+ path: string;
183
+ size: number;
184
+ /** Binary or over the view limit: no `html`, offer `SKILLS_ROUTES.file` instead. */
185
+ binary: boolean;
186
+ tooLarge: boolean;
187
+ /** Server-highlighted, sanitized HTML: one `<span class="line">` per line. */
188
+ html?: string;
189
+ lines?: number;
190
+ }
191
+
192
+ export interface SkillSearchHit {
193
+ scope: string;
194
+ name: string;
195
+ description: string;
196
+ keywords: string[];
197
+ version: string;
198
+ publisher: SkillUser;
199
+ updatedAt: number;
200
+ weeklyDownloads: number;
201
+ deprecated?: string;
202
+ }
203
+
204
+ export type SkillSearchSort = "relevance" | "downloads" | "recent";
205
+
206
+ /** `GET SKILLS_ROUTES.search?q=&sort=&page=`. `q` accepts `owner:<scope>` and `keyword:<kw>` filters. */
207
+ export interface SkillSearchResponse {
208
+ total: number;
209
+ page: number;
210
+ perPage: number;
211
+ hits: SkillSearchHit[];
212
+ }
213
+
214
+ /** `GET SKILLS_ROUTES.home`. */
215
+ export interface SkillHome {
216
+ stats: { packages: number; versions: number; publishers: number; weeklyDownloads: number };
217
+ recent: SkillSearchHit[];
218
+ popular: SkillSearchHit[];
219
+ trending: SkillSearchHit[];
220
+ keywords: [keyword: string, count: number][];
221
+ }
222
+
223
+ /** `GET SKILLS_ROUTES.user`: a user or org scope page. */
224
+ export interface SkillProfile {
225
+ user: SkillUser;
226
+ kind: "user" | "org";
227
+ scopes: string[];
228
+ packages: SkillSearchHit[];
229
+ /** Orgs only. */
230
+ members?: { user: SkillUser; role: SkillOrgRole }[];
231
+ }
232
+
233
+ export type SkillOrgRole = "owner" | "member";
234
+
235
+ export interface SkillPublishResponse {
236
+ scope: string;
237
+ name: string;
238
+ version: string;
239
+ integrity: string;
240
+ /** Package page. */
241
+ url: string;
242
+ /** Dist-tags now pointing at this version. */
243
+ tags: string[];
244
+ }
245
+
246
+ /** Registry publish token (CI). The secret is shown once, at creation. */
247
+ export interface SkillToken {
248
+ id: string;
249
+ name: string;
250
+ /** `@scope/name` packages the token may publish; empty = all packages the creator manages. */
251
+ packages: string[];
252
+ createdAt: number;
253
+ expiresAt?: number;
254
+ lastUsedAt?: number;
255
+ }
256
+
257
+ export interface SkillTokenCreated extends SkillToken {
258
+ /** `sks_…`; never retrievable again. */
259
+ token: string;
260
+ }
261
+
262
+ /** `GET SKILLS_ROUTES.me`. */
263
+ export interface SkillSession {
264
+ user: SkillUser | null;
265
+ /** Sign-in is configured on this server. */
266
+ login: boolean;
267
+ admin: boolean;
268
+ }
269
+
270
+ /** Error body of every non-2xx JSON response. */
271
+ export interface SkillError {
272
+ error: string;
273
+ }
274
+
275
+ const pkg = (scope: string, name: string) => `/api/v1/skills/@${scope}/${name}`;
276
+
277
+ /**
278
+ * HTTP routes, relative to the registry base URL. Mutations with a JSON body
279
+ * use the documented request shapes:
280
+ * - `tag` PUT `{ version }`, DELETE to remove (never `latest`)
281
+ * - `yank` POST `{ yanked: boolean }`
282
+ * - `deprecate` POST `{ message: string | null }`
283
+ * - `owner` PUT / DELETE (no body)
284
+ * - `tokens` POST `{ name, packages?: string[], expiresInDays?: number }`
285
+ * - `report` POST `{ reason: string }`
286
+ * - `takedown` POST `{ reason: string }` (admins)
287
+ * - `orgs` POST `{ name }`; `orgMember` PUT `{ role }` / DELETE
288
+ * - `importSkill` POST body = Claude `.skill` zip (bearer) → {@link SkillPublishResponse}
289
+ */
290
+ export const SKILLS_ROUTES = {
291
+ home: "/api/v1/home",
292
+ search: "/api/v1/search",
293
+ packument: (scope: string, name: string) => pkg(scope, name),
294
+ version: (scope: string, name: string, version: string) => `${pkg(scope, name)}/versions/${version}`,
295
+ readme: (scope: string, name: string, version: string) => `${pkg(scope, name)}/versions/${version}/readme`,
296
+ /** 302 to a signed R2 link; counts one download. */
297
+ tarball: (scope: string, name: string, version: string) => `${pkg(scope, name)}/versions/${version}/tarball`,
298
+ /** Raw file, same-origin: text as text/plain, sniffed raster images inline, everything else as an attachment. */
299
+ file: (scope: string, name: string, version: string, path: string) =>
300
+ `${pkg(scope, name)}/versions/${version}/files/${path}`,
301
+ source: (scope: string, name: string, version: string, path: string) =>
302
+ `${pkg(scope, name)}/versions/${version}/source/${path}`,
303
+ publish: (scope: string, name: string, version: string) => `${pkg(scope, name)}/versions/${version}`,
304
+ tag: (scope: string, name: string, tag: string) => `${pkg(scope, name)}/tags/${tag}`,
305
+ yank: (scope: string, name: string, version: string) => `${pkg(scope, name)}/versions/${version}/yank`,
306
+ deprecate: (scope: string, name: string, version: string) => `${pkg(scope, name)}/versions/${version}/deprecate`,
307
+ owner: (scope: string, name: string, username: string) => `${pkg(scope, name)}/owners/${username}`,
308
+ report: (scope: string, name: string) => `${pkg(scope, name)}/report`,
309
+ takedown: (scope: string, name: string) => `/api/v1/admin/skills/@${scope}/${name}/takedown`,
310
+ user: (username: string) => `/api/v1/users/${username}`,
311
+ tokens: "/api/v1/tokens",
312
+ token: (id: string) => `/api/v1/tokens/${id}`,
313
+ orgs: "/api/v1/orgs",
314
+ orgMember: (org: string, username: string) => `/api/v1/orgs/${org}/members/${username}`,
315
+ importSkill: "/api/v1/import",
316
+ me: "/api/me",
317
+ /** Web pages. */
318
+ pages: {
319
+ package: (scope: string, name: string) => `/@${scope}/${name}`,
320
+ user: (username: string) => `/~${username}`,
321
+ search: (query: string) => `/search?q=${encodeURIComponent(query)}`,
322
+ },
323
+ } as const;
package/src/stream.ts ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Wire types for `omp stream`: Twitch-style live screen sharing at
3
+ * `live.omp.sh/<username>`.
4
+ *
5
+ * Independent from collab. A publisher (`omp stream`) sends plaintext JSON
6
+ * screen deltas for one or more panes (one pane per omp session attached in
7
+ * the same working directory); the stream server materializes each pane
8
+ * (viewport + bounded history) so late viewers receive a snapshot without
9
+ * touching the publisher, fans frames out to viewers, and hosts chat.
10
+ *
11
+ * Rows are terminal lines carrying only SGR and OSC 8 escapes; the publisher
12
+ * strips every other sequence, truncates to the pane width, and redacts
13
+ * secrets before a row leaves the session process.
14
+ */
15
+
16
+ /** Default stream server; its host route derives the channel from the bearer identity. */
17
+ export const DEFAULT_STREAM_URL = "https://live.omp.sh";
18
+
19
+ /** Protocol version carried in `hello`/`snapshot`; the server rejects mismatches. */
20
+ export const STREAM_PROTO = 1;
21
+
22
+ /** Channel names are Stencil usernames: lowercase letters, numbers, and underscores; 3–32 chars. */
23
+ export const STREAM_CHANNEL_NAME_RE = /^[a-z0-9][a-z0-9_]{2,31}$/;
24
+
25
+ /** Longest accepted stream and pane title. */
26
+ export const STREAM_TITLE_MAX = 120;
27
+ /** Longest accepted chat display name. */
28
+ export const STREAM_CHAT_NAME_MAX = 24;
29
+ /** Longest accepted chat message. */
30
+ export const STREAM_CHAT_TEXT_MAX = 500;
31
+ /** History rows the server retains per pane; older rows fall off the top. */
32
+ export const STREAM_HISTORY_LIMIT = 2000;
33
+
34
+ /** One terminal row: ANSI text limited to SGR + OSC 8, width-truncated. */
35
+ export type StreamRow = string;
36
+
37
+ /**
38
+ * Per-pane screen deltas. `pane` ids are assigned by the publisher and are
39
+ * unique for the lifetime of one host connection.
40
+ *
41
+ * - `history` appends rows committed above the viewport (append-only).
42
+ * - `viewport` replaces the whole live viewport.
43
+ * - `patch` rewrites individual viewport rows; `rows` is the new viewport
44
+ * length (shrinks drop trailing rows, growth fills with empty rows before
45
+ * ops apply).
46
+ * - `reset` clears history and viewport (the session cleared its screen).
47
+ */
48
+ export type StreamPaneFrame =
49
+ | { t: "pane-open"; pane: number; title: string; cols: number; rows: number }
50
+ | { t: "pane-close"; pane: number }
51
+ | { t: "resize"; pane: number; cols: number; rows: number }
52
+ | { t: "history"; pane: number; rows: StreamRow[] }
53
+ | { t: "viewport"; pane: number; rows: StreamRow[] }
54
+ | { t: "patch"; pane: number; ops: [index: number, row: StreamRow][]; rows: number }
55
+ | { t: "reset"; pane: number }
56
+ | { t: "paused"; pane: number; paused: boolean };
57
+
58
+ /** Publisher → server. `hello` is the first frame on the socket. */
59
+ export type StreamHostFrame =
60
+ | { t: "hello"; proto: number; title: string }
61
+ | { t: "title"; title: string }
62
+ /** Message typed by the streamer; broadcast with `host: true`. */
63
+ | { t: "chat"; text: string }
64
+ | StreamPaneFrame;
65
+
66
+ export interface StreamChatMessage {
67
+ /** Monotonic per channel-session; viewers use it for de-duplication. */
68
+ id: number;
69
+ name: string;
70
+ text: string;
71
+ /** Unix milliseconds. */
72
+ ts: number;
73
+ /** Set when the streamer sent it. */
74
+ host?: boolean;
75
+ }
76
+
77
+ /** Server → publisher. */
78
+ export type StreamServerToHost =
79
+ /** `user` is the stencil.so username the bearer resolved to; shown as the host's chat name. */
80
+ | { t: "welcome"; proto: number; channel: string; url: string; user?: string }
81
+ | { t: "viewers"; n: number }
82
+ | { t: "chat"; msg: StreamChatMessage }
83
+ | { t: "error"; message: string };
84
+
85
+ /** Directory entry served by `GET /api/channels` and `GET /api/channels/<name>`. */
86
+ export interface StreamChannelInfo {
87
+ name: string;
88
+ title: string;
89
+ live: boolean;
90
+ viewers: number;
91
+ panes: number;
92
+ /** stencil.so username of the channel owner (the first authenticated host). */
93
+ owner?: string;
94
+ /** Unix milliseconds of the current live session; absent when offline. */
95
+ startedAt?: number;
96
+ }
97
+
98
+ /** Materialized pane state delivered to a joining viewer. */
99
+ export interface StreamPaneSnapshot {
100
+ id: number;
101
+ title: string;
102
+ cols: number;
103
+ rows: number;
104
+ history: StreamRow[];
105
+ viewport: StreamRow[];
106
+ paused: boolean;
107
+ }
108
+
109
+ /**
110
+ * Server → viewer. `snapshot` is the first frame after connect and again
111
+ * whenever the publisher reconnects; `offline` means the publisher left and
112
+ * the viewer should keep the socket open for the next `snapshot`.
113
+ */
114
+ export type StreamServerToViewer =
115
+ | {
116
+ t: "snapshot";
117
+ proto: number;
118
+ channel: StreamChannelInfo;
119
+ panes: StreamPaneSnapshot[];
120
+ chat: StreamChatMessage[];
121
+ }
122
+ | { t: "offline" }
123
+ | { t: "title"; title: string }
124
+ | { t: "viewers"; n: number }
125
+ | { t: "chat"; msg: StreamChatMessage }
126
+ | StreamPaneFrame;
127
+
128
+ /** Viewer → server. The display name travels with each message until accounts exist. */
129
+ export type StreamViewerFrame = { t: "chat"; name: string; text: string };
130
+
131
+ /** WebSocket close codes used by the stream server. */
132
+ export const STREAM_CLOSE_HOST_CONFLICT = 4009;
133
+ export const STREAM_CLOSE_BAD_CHANNEL = 4004;
134
+ export const STREAM_CLOSE_PROTO_MISMATCH = 4010;
135
+ /** Host bearer token missing, expired, or not issued by the stencil.so issuer. */
136
+ export const STREAM_CLOSE_UNAUTHORIZED = 4401;
137
+ /** Channel is owned by a different stencil.so account. */
138
+ export const STREAM_CLOSE_FORBIDDEN = 4403;
139
+
140
+ /** Provider id under which `/login` stores the stencil.so credential; `STENCIL_API_KEY` overrides it. */
141
+ export const STREAM_AUTH_PROVIDER = "stencil";
142
+ export const STREAM_AUTH_ENV = "STENCIL_API_KEY";
143
+
144
+ /** Longest accepted clip description (runes). Titles share `STREAM_TITLE_MAX`. */
145
+ export const CLIP_DESCRIPTION_MAX = 5000;
146
+
147
+ /**
148
+ * `POST /api/clips` answer. The body is an `.ompcast` recording (optionally
149
+ * `Content-Encoding: gzip`) whose header may carry `title` and `description`;
150
+ * the bearer identifies the uploading Stencil account.
151
+ */
152
+ export interface ClipUploadResponse {
153
+ id: string;
154
+ /** Public clip page, `<server>/c/<id>`. */
155
+ url: string;
156
+ }
157
+
158
+ /** HTTP/WS route layout of the stream server, relative to `DEFAULT_STREAM_URL`. */
159
+ export const STREAM_ROUTES = {
160
+ channels: "/api/channels",
161
+ channel: (name: string) => `/api/channels/${name}`,
162
+ /** The server derives the host channel from the authenticated username. */
163
+ host: "/ws/host",
164
+ watch: (name: string) => `/ws/watch/${name}`,
165
+ page: (name: string) => `/${name}`,
166
+ /** Clip upload (bearer-authenticated `POST`). */
167
+ clips: "/api/clips",
168
+ clipPage: (id: string) => `/c/${id}`,
169
+ } as const;