@hypit/hypit 0.2.1 → 0.2.3

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.
Files changed (41) hide show
  1. package/README.md +38 -4
  2. package/bin/hypit.mjs +12 -9
  3. package/dist/public/generation.d.ts +4 -5
  4. package/examples/provider-package/README.md +92 -16
  5. package/examples/provider-package/hypit.runtime.json +8 -1
  6. package/examples/provider-package/packages/provider-videos/package.json +11 -0
  7. package/examples/provider-package/packages/provider-videos/src/activation.ts +24 -0
  8. package/examples/provider-package/packages/provider-videos/src/provider.ts +181 -0
  9. package/examples/provider-package/packages/provider-videos/tsconfig.json +9 -0
  10. package/package.json +1 -1
  11. package/packages/build-result/package.json +1 -1
  12. package/packages/build-result/src/store.ts +1 -1
  13. package/packages/caption-fine-studio/package.json +1 -1
  14. package/packages/cli/src/commands/environment.ts +12 -5
  15. package/packages/cli/src/output.ts +1 -1
  16. package/packages/credential-store-file/README.md +9 -4
  17. package/packages/credential-store-file/package.json +2 -1
  18. package/packages/credential-store-file/src/activation.ts +3 -2
  19. package/packages/credential-store-file/src/store.ts +3 -2
  20. package/packages/credential-store-os/runtime/windows-credential.ps1 +1 -2
  21. package/packages/credential-store-os/src/store.ts +4 -61
  22. package/packages/credential-store-os/src/windows.ts +47 -0
  23. package/packages/credential-store-platform/README.md +2 -1
  24. package/packages/credential-store-platform/src/activation.ts +1 -3
  25. package/packages/file-io-node/README.md +20 -0
  26. package/packages/file-io-node/package.json +13 -0
  27. package/packages/file-io-node/src/index.ts +1 -0
  28. package/packages/{build-result → file-io-node}/src/replace-file-windows.ts +3 -3
  29. package/packages/generation/README.md +6 -0
  30. package/packages/generation/src/mapping.ts +30 -10
  31. package/packages/media-execution/src/process-env.ts +3 -2
  32. package/packages/media-execution/src/toolchain.ts +10 -36
  33. package/packages/performance/package.json +2 -1
  34. package/packages/provider-hyperframes-local/src/process.ts +6 -2
  35. package/packages/provider-hyperframes-local/src/program.ts +2 -1
  36. package/packages/provider-media-local/README.md +2 -1
  37. package/packages/runtime-local/src/programs.ts +10 -5
  38. package/packages/video-cli/README.md +6 -4
  39. package/packages/video-cli/package.json +1 -0
  40. package/packages/video-cli/src/distribution.ts +5 -2
  41. /package/packages/{build-result → file-io-node}/src/replace-file.ts +0 -0
package/README.md CHANGED
@@ -147,11 +147,39 @@ commands and the repository layout.
147
147
  ## Launch Partner
148
148
 
149
149
  <table>
150
+ <tr>
151
+ <td width="220" align="center">
152
+ <a href="https://watcha.cn">
153
+ <picture>
154
+ <source media="(prefers-color-scheme: dark)" srcset="./docs/public/watcha-dark.png">
155
+ <img alt="Watcha" src="./docs/public/watcha-light.png" width="170">
156
+ </picture>
157
+ </a>
158
+ </td>
159
+ <td><a href="https://watcha.cn">Watcha</a> is an innovative ecosystem and community built for AI builders, connecting great products with high-quality users to turn every Demo into reality. Starting from AI product discovery and genuine feedback, Watcha provides product publishing, distribution, and reputation tracking to support builders from launch to early adoption.</td>
160
+ </tr>
161
+ <tr>
162
+ <td width="220" align="center">
163
+ <a href="https://tokendance.space/?utm_source=hypit">
164
+ <picture>
165
+ <source media="(prefers-color-scheme: dark)" srcset="./docs/public/tokendance-dark.png">
166
+ <img alt="TokenDance" src="./docs/public/tokendance-light.png" width="165">
167
+ </picture>
168
+ </a>
169
+ </td>
170
+ <td><a href="https://tokendance.space/?utm_source=hypit">TokenDance</a> offers builders a unified multi-model gateway with optimized inference costs. Through programs like the Billion-Token Subsidy and the Developer Program, TokenDance helps early-stage builders cut costs so they can focus on product validation and growth.</td>
171
+ </tr>
172
+ <tr>
173
+ <td width="220" align="center">
174
+ <a href="https://autoclaw.z.ai"><img alt="AutoClaw" src="./docs/public/autoclaw.png" width="180"></a>
175
+ </td>
176
+ <td><a href="https://autoclaw.z.ai">AutoClaw</a> is Z.ai’s desktop AI assistant for work, deeply optimized for GLM models and integrated with specialized knowledge bases and workflows for legal, finance, design, and more.</td>
177
+ </tr>
150
178
  <tr>
151
179
  <td width="220" align="center">
152
180
  <a href="https://openagents.org/blog/posts/2026-09-08-pilot-user-program-zh-hypit"><img alt="OpenAgents" src="./docs/public/openagents.png" width="180"></a>
153
181
  </td>
154
- <td>OpenAgents Workspace is a product that helps you manage AI agents, allowing you to invoke agents running on any device (whether on a mobile phone or computer) anytime, anywhere. Agents can collaborate within the platform to help you complete tasks and boost efficiency. It currently supports integration with over 20 types of agents, including Claude, Codex, OpenClaw, and Hermes (<a href="https://openagents.org">https://openagents.org</a>)</td>
182
+ <td><a href="https://openagents.org">OpenAgents Workspace</a> is a product that helps you manage AI agents, allowing you to invoke agents running on any device (whether on a mobile phone or computer) anytime, anywhere. Agents can collaborate within the platform to help you complete tasks and boost efficiency. It currently supports integration with over 20 types of agents, including Claude, Codex, OpenClaw, and Hermes.</td>
155
183
  </tr>
156
184
  <tr>
157
185
  <td width="220" align="center">
@@ -162,13 +190,19 @@ commands and the repository layout.
162
190
  </picture>
163
191
  </a>
164
192
  </td>
165
- <td>Monid is the OpenRouter for agent tools: one key, one wallet, hundreds of tools, billed per call. Hypit can route Seedance video, images, speech and word-level transcription to Monid.</td>
193
+ <td><a href="https://monid.ai?fpr=alvin-45480b">Monid</a> is the OpenRouter for agent tools: one key, one wallet, hundreds of tools, billed per call. Hypit can route Seedance video, images, speech and word-level transcription to Monid.</td>
194
+ </tr>
195
+ <tr>
196
+ <td width="220" align="center">
197
+ <a href="https://www.hiapi.ai/invite/hypit"><img alt="HiAPI" src="./docs/public/hiapi.png" width="165"></a>
198
+ </td>
199
+ <td><a href="https://www.hiapi.ai/invite/hypit">HiAPI</a> is a unified API platform for AI image, video, audio, and language models. Connect GPT Image 2.5, Seedance 2.0, Veo 3.1, Kling, Wan, and other popular models through one platform. Pay as you go with no monthly fee, with free trial credits available for new users.</td>
166
200
  </tr>
167
201
  <tr>
168
202
  <td width="220" align="center">
169
- <a href="https://www.hiapi.ai"><img alt="HiAPI" src="./docs/public/hiapi.png" width="165"></a>
203
+ <a href="https://api.pollo.ai/?utm_source=hypit"><img alt="Pollo API" src="./docs/public/pollo.jpg" width="120"></a>
170
204
  </td>
171
- <td>HiAPI is a unified API platform for AI image, video, audio, and language models. Connect GPT Image 2.5, Seedance 2.0, Veo 3.1, Kling, Wan, and other popular models through one platform. Pay as you go with no monthly fee, with free trial credits available for new users.</td>
205
+ <td><a href="https://api.pollo.ai/?utm_source=hypit">Pollo API</a> gives developers access to 300+ AI image and video models through a single API, with competitive rates. One API key connects you to multiple model providers and scales from 10 to 10,000+ requests.</td>
172
206
  </tr>
173
207
  </table>
174
208
 
package/bin/hypit.mjs CHANGED
@@ -1,9 +1,9 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import { register } from "tsx/esm/api";
4
- import { readFileSync } from "node:fs";
5
- import { dirname, resolve } from "node:path";
6
- import { fileURLToPath } from "node:url";
4
+ import { readFileSync, realpathSync } from "node:fs";
5
+ import { dirname, resolve, sep } from "node:path";
6
+ import { fileURLToPath, pathToFileURL } from "node:url";
7
7
 
8
8
  if (process.argv.length === 3 && ["--version", "-v"].includes(process.argv[2])) {
9
9
  const manifest = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
@@ -18,27 +18,30 @@ process.emitWarning = function hypitWarning(warning, ...args) {
18
18
  return emitWarning.call(process, warning, ...args);
19
19
  };
20
20
 
21
- const distributionRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
21
+ // Bootstrap and package activation must agree on the physical Distribution root. Windows short
22
+ // paths can survive Node's ordinary resolution while package lookup expands them through libuv.
23
+ const distributionRoot = realpathSync.native(resolve(dirname(fileURLToPath(import.meta.url)), ".."));
24
+ const distributionUrl = pathToFileURL(distributionRoot + sep);
22
25
  register();
23
26
  const {
24
27
  installDistributionPackageResolution,
25
28
  installExternalPackageResolution,
26
- } = await import("../packages/package-loader-node/src/distribution-resolution.ts");
29
+ } = await import(new URL("packages/package-loader-node/src/distribution-resolution.ts", distributionUrl).href);
27
30
  installDistributionPackageResolution([distributionRoot]);
28
- const { hypitHostPackageRoot } = await import("../packages/runtime-host-node/src/index.ts");
31
+ const { hypitHostPackageRoot } = await import(new URL("packages/runtime-host-node/src/index.ts", distributionUrl).href);
29
32
  installExternalPackageResolution([hypitHostPackageRoot()]);
30
33
  const args = process.argv.slice(2);
31
34
  if (args[0] === "studio" || (args[0] === "help" && args[1] === "studio")) {
32
- const { runStudio } = await import("../packages/studio/start.ts");
35
+ const { runStudio } = await import(new URL("packages/studio/start.ts", distributionUrl).href);
33
36
  try {
34
37
  await runStudio(args[0] === "help" ? ["--help"] : args.slice(1).filter((arg) => arg !== "--debug"), {
35
38
  write: (text) => process.stdout.write(text),
36
39
  });
37
40
  } catch (error) {
38
- const { renderCliError } = await import("../packages/cli/src/index.ts");
41
+ const { renderCliError } = await import(new URL("packages/cli/src/index.ts", distributionUrl).href);
39
42
  process.stderr.write(renderCliError(error, { debug: args.includes("--debug") }));
40
43
  process.exitCode = 1;
41
44
  }
42
45
  } else {
43
- await import("../packages/video-cli/src/cli.ts");
46
+ await import(new URL("packages/video-cli/src/cli.ts", distributionUrl).href);
44
47
  }
@@ -533,14 +533,13 @@ declare function selectWireModelForRequest(mapping: GenerationWireMapping, reque
533
533
  * only the mapping and the request, never the model package.
534
534
  */
535
535
  declare function compileWireRequest(mapping: GenerationWireMapping, request: GenerationRequest, resolve: GenerationArtifactUrlResolver): Promise<GenerationWireRequest>;
536
- /** Does this request only use ports this mapping can write? Needs no model package. */
536
+ /** Optional fields may be omitted by authors, but supplied fields must reach the service. */
537
537
  declare function mappingSupportsRequest(mapping: GenerationWireMapping, value: unknown): boolean;
538
538
  /**
539
- * Prove one service mapping covers every port the model declares.
539
+ * Check structural port coverage and required item fields against the model declaration.
540
540
  *
541
- * This is the check the old hand-written per-model translators could not have:
542
- * a forgotten reference role or item field used to surface only after a paid
543
- * generation returned the wrong result.
541
+ * A service may omit optional item capabilities. mappingSupportsRequest and final compilation
542
+ * additionally refuse requests that actually supply such an unmapped field.
544
543
  */
545
544
  declare function assertMappingCoversPorts(table: GenerationPortTable, mapping: GenerationWireMapping): void;
546
545
 
@@ -1,10 +1,23 @@
1
1
  # A project-owned Provider
2
2
 
3
- This complete package demonstrates an asynchronous image service through the public Hypit SDK.
4
- The service protocol below is **illustrative**, not a live vendor API. Replace its mapping and
5
- transport with the selected service's documented operations. The lifecycle test exercises the
6
- example without a paid request. Implement the capabilities needed by the production; a service's
7
- entire catalogue is not required.
3
+ This example shows a service that Hypit's official Distribution does not ship, implemented as a
4
+ project package through the public Hypit SDK. Two packages cover the two request shapes a service
5
+ usually differs on:
6
+
7
+ | Package | Model | Request | Lifecycle |
8
+ | --- | --- | --- | --- |
9
+ | `packages/provider-images` | `@hypit/gpt-image@1#gpt-image-2` | one image, optional references | asynchronous |
10
+ | `packages/provider-videos` | `@hypit/seedance@1#seedance-2-mini` | one video, references and frames | asynchronous |
11
+
12
+ Both service protocols are **illustrative**, not live vendor APIs. Replace the mapping and transport
13
+ with the selected service's documented operations. The lifecycle tests exercise the examples without
14
+ a paid request. Implement the capabilities needed by the production; a service's entire catalogue is
15
+ not required.
16
+
17
+ The video example is the one to read when the service renders **video**. A video request carries the
18
+ whole reference vocabulary — images, videos, audio and first/last frames — and the result is a large
19
+ remote task rather than a returned file. This is the shape most third-party video services use, and
20
+ the shape the official Distribution cannot supply for a service it does not know.
8
21
 
9
22
  ## Follow one request
10
23
 
@@ -46,25 +59,84 @@ does not claim remote cancellation.
46
59
  source and preserves the response with a concise description. It reads no graph and invents no
47
60
  future media duration. Rates and credentials do not supply spending permission.
48
61
 
62
+ ## Follow a video request
63
+
64
+ A Seedance request reaches `@hypit/seedance@1#seedance-2-mini` and returns a generated video-set.
65
+ The Model declares the request; this Provider decides how the service receives it. Four things
66
+ differ from the image example, and each is the reason the video example exists.
67
+
68
+ **The reference vocabulary is wider, and each role maps to its own field.** The Model declares
69
+ `referenceImage`, `referenceVideo` and `referenceAudio` as separate ports precisely so each can map
70
+ to one wire field — a service that mixes them into one array cannot tell the roles apart. An image
71
+ reference also carries an optional `personReference` classification, which is why those ports use
72
+ `itemObject` rather than `urlArray`: the item field travels with the URL it belongs to.
73
+
74
+ ```json
75
+ { "references": [{ "url": "https://…/reference.png", "person": true }] }
76
+ ```
77
+
78
+ `firstFrame` and `lastFrame` accept at most one item each, so their body fields are URLs.
79
+ This illustrative service accepts their optional person classification on `POST /uploads` through
80
+ `x-person-reference: true` or `false`. Their mappings declare `resourceFields: ["personReference"]`,
81
+ and the URL resolver sends that header when supplied. Replace this with the real service’s
82
+ documented transport; a field that the service cannot carry must be refused, never discarded.
83
+
84
+ **The service's range is narrower than the Model's, and the Provider states that.** Seedance 2 Mini
85
+ admits 480p/720p and 4–15 seconds; this illustrative service renders 720p up to 10 seconds. The
86
+ Model is not edited for that. `supports` reports the difference before submission, so `plan` refuses
87
+ the request with a reason instead of failing a paid job downstream:
88
+
89
+ ```
90
+ This service renders at most 10 seconds, not 12
91
+ ```
92
+
93
+ Do not widen the Model to fit one service. Do not silently clamp the author's number either — the
94
+ author wrote 12 seconds, and a 10-second result is a different video.
95
+
96
+ **Submission is an enqueue, not the render.** `start` returns as soon as the service acknowledges a
97
+ task, and the HTTP bound covers the API call, not the minutes the render may take. `poll` returns
98
+ `pending` while the task is `queued` or `running`, and the task id is checkpointed before `start`
99
+ returns, so an interrupted Build still names the remote work it began.
100
+
101
+ **The result is fetched in a separate `collect` step.** A finished task carries a signed URL; the
102
+ account key goes only to the service's own API. `collect` downloads the media, stores it through
103
+ `context.resources` and returns the Model's declared value. Because `poll` and `collect` are
104
+ separate actions, download capacity is configured independently of task capacity.
105
+
106
+ A task that reports `succeeded` without an output URL is a service contract violation, not a pending
107
+ job, and fails loudly. A `failed` task keeps the service's own error code and the received task id
108
+ as evidence, and redacts URLs from the message it republishes.
109
+
110
+ `assertMappingCoversPorts` checks declared ports and required item fields. Optional fields need
111
+ request-level checks and transport tests as well:
112
+ a mapping that forgets a reference role or an item field fails at load rather than after a paid
113
+ generation returns the wrong video. Where a Model port is genuinely optional, `whenAbsent` states
114
+ what the service should receive when the author omits it — the service then gets the value the
115
+ author's request implies, not a field left to its own default. The lifecycle test asserts the
116
+ mapping against the Model's own port table.
117
+
49
118
  ## Make it a package in the production
50
119
 
51
- Copy `packages/provider-images` into the video's `packages/`. Choose your own package name and change
52
- `providerModule.name` with it. Implement the actual service protocol, including its request limits,
53
- upload/download limits and any OAuth or cancellation behavior it really offers. Configure the
54
- selected service address; `images.example` is a placeholder that cannot generate media.
120
+ Copy the package matching your service's request shape `packages/provider-images` or
121
+ `packages/provider-videos` into the video's `packages/`. Choose your own package name and change
122
+ `providerModule.name` with it. A service that renders both images and video can implement both
123
+ capabilities in one package; `defineEndpointPackage` accepts several. Implement the actual service
124
+ protocol, including its request limits, upload/download limits and any OAuth or cancellation
125
+ behavior it really offers. Configure the selected service address; `images.example` and
126
+ `videos.example` are placeholders that cannot generate media.
55
127
 
56
128
  Use the active `@hypit/hypit` version as a development dependency. Build and install with the
57
- project's package manager. In this repository the example uses `workspace:*` for that dependency;
58
- replace it with your selected release when copying it out. The `@hypit/driver-node` and
59
- `@hypit/gpt-image` development dependencies serve the repository test only and can be removed from
60
- the copied package. For example:
129
+ project's package manager. In this repository the examples use `workspace:*` for that dependency;
130
+ replace it with your selected release when copying it out. The `@hypit/driver-node` and model
131
+ development dependencies (`@hypit/gpt-image`, `@hypit/seedance`) serve the repository tests only and
132
+ can be removed from the copied package. For example:
61
133
 
62
134
  ```bash
63
- cd packages/provider-images
135
+ cd packages/provider-videos
64
136
  npm install
65
137
  npm run build
66
138
  cd ../..
67
- npm install ./packages/provider-images
139
+ npm install ./packages/provider-videos
68
140
  ```
69
141
 
70
142
  The package ships JavaScript. Its `hypit.activation` exports a Profile-selected Runtime facet;
@@ -73,10 +145,14 @@ loading project packages. Keep the chosen package version and lockfile with the
73
145
 
74
146
  Merge the endpoint, credential store and binding from `hypit.runtime.json` into the project's
75
147
  chosen Profile. Preserve its other services and resolve bindings explicitly. Once the user chooses
76
- that account, `hypit auth login images.personal` securely enters the key into its declared store.
148
+ that account, `hypit auth login videos.personal` securely enters the key into its declared store.
77
149
  `plan` checks the requested parameters; `pricing` supplies the service's rates. Submitting a real
78
150
  generation follows the agreed production scope and spending authority.
79
151
 
152
+ The service decides whether an Endpoint needs its own credential slot at all. A service that
153
+ authenticates through the platform it runs on, or one reached at a private address, may declare no
154
+ credentials or a non-secret account name; `defineEndpointPackage` takes whichever the service has.
155
+
80
156
  ## Owners
81
157
 
82
158
  - `src/provider.ts`: exact capability, mapping, service support, task lifecycle, pricing and capacity.
@@ -6,7 +6,14 @@
6
6
  "images.personal": {
7
7
  "use": "@example/provider-images",
8
8
  "config": { "baseUrl": "https://images.example", "apiKey": { "store": "platform", "key": "images.personal" } }
9
+ },
10
+ "videos.personal": {
11
+ "use": "@example/provider-videos",
12
+ "config": { "baseUrl": "https://videos.example", "apiKey": { "store": "platform", "key": "videos.personal" } }
9
13
  }
10
14
  },
11
- "bindings": { "@hypit/gpt-image@1#gpt-image-2": "images.personal" }
15
+ "bindings": {
16
+ "@hypit/gpt-image@1#gpt-image-2": "images.personal",
17
+ "@hypit/seedance@1#seedance-2-mini": "videos.personal"
18
+ }
12
19
  }
@@ -0,0 +1,11 @@
1
+ {
2
+ "name": "@example/provider-videos",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "hypit": { "activation": "./dist/activation.js" },
7
+ "exports": { ".": "./dist/provider.js" },
8
+ "files": ["dist"],
9
+ "scripts": { "build": "tsc -p tsconfig.json" },
10
+ "devDependencies": { "@hypit/hypit": "workspace:*", "typescript": "5.9.3", "@hypit/driver-node": "workspace:*", "@hypit/seedance": "workspace:*" }
11
+ }
@@ -0,0 +1,24 @@
1
+ import {
2
+ createRuntimeEndpointAdapterFacet, runtimeConfigCredentialRef, runtimeConfigExact,
3
+ runtimeConfigObject, runtimeConfigPositiveInteger, runtimeConfigString,
4
+ } from "@hypit/hypit/runtime-kit";
5
+ import { createVideoProvider, providerModule } from "./provider.js";
6
+
7
+ export default {
8
+ format: "hypit.node-package@1" as const,
9
+ hostFacets: [createRuntimeEndpointAdapterFacet({
10
+ use: providerModule.name,
11
+ activate(context) {
12
+ const config = runtimeConfigObject(context.config, "Video service");
13
+ runtimeConfigExact(config, ["baseUrl", "apiKey", "concurrency", "pollIntervalMs"], "Video service");
14
+ const baseUrl = runtimeConfigString(config.baseUrl, "Video service baseUrl");
15
+ const apiKey = runtimeConfigCredentialRef(config.apiKey, "Video service apiKey");
16
+ if (!baseUrl || !apiKey || !context.pool) throw new Error("Video service requires baseUrl, apiKey and pool");
17
+ return { endpoint: createVideoProvider({
18
+ instance: context.instance, pool: context.pool, baseUrl, apiKey,
19
+ concurrency: runtimeConfigPositiveInteger(config.concurrency, "concurrency") ?? 1,
20
+ pollIntervalMs: runtimeConfigPositiveInteger(config.pollIntervalMs, "pollIntervalMs") ?? 5_000,
21
+ }) };
22
+ },
23
+ })],
24
+ };
@@ -0,0 +1,181 @@
1
+ import { canonicalize, defineEndpointPackage, wakeAfter } from "@hypit/hypit/endpoint-kit";
2
+ import type { AsyncEndpoint, CredentialRef, EndpointRequest } from "@hypit/hypit/endpoint-kit";
3
+ import {
4
+ compileWireRequest, generationTypes, mappingSupportsRequest,
5
+ sealGeneratedVideoSet, selectWireModelForRequest,
6
+ } from "@hypit/hypit/generation";
7
+ import type { GenerationRequest, GenerationWireMapping } from "@hypit/hypit/generation";
8
+
9
+ export const providerModule = { name: "@example/provider-videos", version: "1" } as const;
10
+ export const capability = { module: { name: "@hypit/seedance", version: "1" }, name: "seedance-2-mini" } as const;
11
+
12
+ // This example service implements only this subset of the model. Its API is described in README.
13
+ export const mapping: GenerationWireMapping = {
14
+ capability, result: "video", routes: [{ model: "seedance-2-mini" }],
15
+ fields: {
16
+ prompt: { as: "value", field: "prompt" },
17
+ duration: { as: "value", field: "duration" },
18
+ resolution: { as: "value", field: "resolution" },
19
+ aspectRatio: { as: "value", field: "ratio" },
20
+ generateAudio: { as: "value", field: "audio" },
21
+ webSearch: { as: "value", field: "search" },
22
+ referenceImage: { as: "itemObject", field: "references", urlKey: "url", fieldKeys: { personReference: "person" } },
23
+ referenceVideo: { as: "itemObject", field: "videoReferences", urlKey: "url", fieldKeys: { personReference: "person" } },
24
+ referenceAudio: { as: "urlArray", field: "audioReferences" },
25
+ firstFrame: { as: "url", field: "firstFrame", resourceFields: ["personReference"] },
26
+ lastFrame: { as: "url", field: "lastFrame", resourceFields: ["personReference"] },
27
+ },
28
+ };
29
+
30
+ // This service renders a narrower band than the model vocabulary allows, which it reports rather
31
+ // than widening: Seedance 2 Mini declares 480p/720p and 4–15 seconds, and this service renders 720p
32
+ // up to 10 seconds. The Model is unchanged; the difference belongs to this Endpoint.
33
+ const SUPPORTED_RESOLUTION = "720p";
34
+ const SUPPORTED_SECONDS = 10;
35
+
36
+ function object(value: unknown): Record<string, unknown> {
37
+ if (value === null || typeof value !== "object" || Array.isArray(value)) throw new Error("Expected service object");
38
+ return value as Record<string, unknown>;
39
+ }
40
+ function text(value: unknown): string {
41
+ if (typeof value !== "string" || value.length === 0) throw new Error("Expected nonempty service text");
42
+ return value;
43
+ }
44
+ // This illustrative service documents these fields as its public failure evidence.
45
+ function publicFailure(value: unknown): { code: string; message: string } | undefined {
46
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return undefined;
47
+ const error = value as Record<string, unknown>;
48
+ if (typeof error.code !== "string" || typeof error.message !== "string") return undefined;
49
+ return { code: error.code, message: error.message.replace(/https?:\/\/\S+/giu, "[redacted-url]") };
50
+ }
51
+ function address(value: string): string {
52
+ const url = new URL(value);
53
+ if (url.protocol !== "https:" && !(url.protocol === "http:" && ["localhost", "127.0.0.1"].includes(url.hostname))) {
54
+ throw new Error("Service URLs require HTTPS or loopback HTTP");
55
+ }
56
+ return url.href;
57
+ }
58
+
59
+ function serviceSupport(request: EndpointRequest) {
60
+ const ports = (request.constraints as unknown as GenerationRequest).ports;
61
+ const duration = ports.duration?.[0];
62
+ if (typeof duration === "number" && duration > SUPPORTED_SECONDS) {
63
+ return { status: "unsupported" as const,
64
+ reason: `This service renders at most ${SUPPORTED_SECONDS} seconds, not ${duration}` };
65
+ }
66
+ const resolution = ports.resolution?.[0];
67
+ if (resolution !== undefined && String(resolution) !== SUPPORTED_RESOLUTION) {
68
+ return { status: "unsupported" as const,
69
+ reason: `This service renders at ${SUPPORTED_RESOLUTION}, not ${String(resolution)}` };
70
+ }
71
+ return mappingSupportsRequest(mapping, request.constraints)
72
+ ? { status: "supported" as const }
73
+ : { status: "unsupported" as const, reason: "This service does not accept one of the requested inputs" };
74
+ }
75
+
76
+ export function createVideoProvider(options: {
77
+ instance: string; pool: string; baseUrl: string; apiKey: CredentialRef;
78
+ concurrency?: number; pollIntervalMs?: number; fetch?: typeof globalThis.fetch;
79
+ }) {
80
+ const base = address(options.baseUrl).replace(/\/$/u, "");
81
+ const fetcher = options.fetch ?? globalThis.fetch;
82
+ const interval = options.pollIntervalMs ?? 5_000;
83
+ const key = (credentials: Readonly<Record<string, { secret: string }>>) => text(credentials.apiKey?.secret);
84
+ async function json(path: string, secret: string, init: RequestInit = {}) {
85
+ const response = await fetcher(`${base}${path}`, {
86
+ ...init, headers: { ...init.headers, authorization: `Bearer ${secret}` },
87
+ // Submission enqueues a remote job and answers immediately; the bound is for the API, not the render.
88
+ signal: AbortSignal.timeout(120_000),
89
+ });
90
+ if (!response.ok) {
91
+ let error: ReturnType<typeof publicFailure>;
92
+ try { error = publicFailure(object(await response.json()).error); }
93
+ catch { /* A missing public error body leaves the HTTP evidence intact. */ }
94
+ const requestId = response.headers.get("x-request-id");
95
+ throw Object.assign(new Error(`Video service ${init.method ?? "GET"} ${path} returned HTTP ${response.status}`
96
+ + (requestId === null ? "" : `; request=${requestId}`)
97
+ + (error === undefined ? "" : `; ${error.code}: ${error.message}`)),
98
+ error === undefined ? {} : { code: error.code });
99
+ }
100
+ return object(await response.json());
101
+ }
102
+ const endpoint: AsyncEndpoint = {
103
+ async start(context) {
104
+ const supported = serviceSupport(context.need);
105
+ if (supported.status === "unsupported") throw new Error(supported.reason);
106
+ const secret = key(context.credentials);
107
+ const authored = context.need.constraints as unknown as GenerationRequest;
108
+ const model = selectWireModelForRequest(mapping, authored);
109
+ // This service has no catalogue query; its known request limits were checked above.
110
+ await context.reportProgress?.({ phase: `Preparing video request: ${model}` });
111
+ const request = await compileWireRequest(mapping, authored,
112
+ async (artifact, fields) => {
113
+ const bytes = await context.resources.get(artifact.resource);
114
+ if (bytes === undefined) throw new Error("Reference media is unavailable");
115
+ const upload = await fetcher(`${base}/uploads`, {
116
+ method: "POST", headers: { "content-type": artifact.mediaType, authorization: `Bearer ${secret}`,
117
+ ...(fields?.personReference === undefined ? {} : { "x-person-reference": String(fields.personReference) }),
118
+ },
119
+ body: new Blob([new Uint8Array(bytes)]), signal: AbortSignal.timeout(120_000),
120
+ });
121
+ if (!upload.ok) throw new Error(`Video service POST /uploads returned HTTP ${upload.status}`);
122
+ return address(text(object(await upload.json()).url));
123
+ });
124
+ await context.reportProgress?.({ phase: `Submitting video request: ${model}` });
125
+ const task = await json("/videos", secret, {
126
+ method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(request),
127
+ });
128
+ const id = text(task.id), handle = { id }, receipt = { id };
129
+ await context.checkpoint?.({ handle, receipt });
130
+ return { ...wakeAfter(handle, interval), receipt };
131
+ },
132
+ async poll(context) {
133
+ const id = text(object(context.handle).id);
134
+ const task = await json(`/videos/${encodeURIComponent(id)}`, key(context.credentials));
135
+ const status = text(task.status);
136
+ if (status === "queued" || status === "running") {
137
+ return wakeAfter({ id }, interval, Date.now(), { phase: status });
138
+ }
139
+ if (status === "failed") {
140
+ const error = publicFailure(task.error);
141
+ return { status: "failed", receipt: { id }, failure: {
142
+ code: error?.code ?? "VIDEO_SERVICE_FAILED",
143
+ message: `Video service task ${id} failed${error === undefined ? "" : `: ${error.message}`}`,
144
+ } };
145
+ }
146
+ if (status !== "succeeded") throw new Error("Video service returned an unknown task state");
147
+ // A completed task without a result URL is a service contract violation, not a pending job.
148
+ return { status: "ready", handle: { id, url: address(text(task.output)) } };
149
+ },
150
+ async collect(context) {
151
+ // The service returns a signed asset URL; account credentials go only to its API.
152
+ const url = address(text(object(context.handle).url));
153
+ await context.reportProgress?.({ phase: "Receiving generated video" });
154
+ // One delivered video is bounded here; a larger file belongs with a streaming resource port.
155
+ const response = await fetcher(url, { signal: AbortSignal.timeout(600_000) });
156
+ if (!response.ok) throw new Error(`Video download returned HTTP ${response.status}`);
157
+ const mediaType = response.headers.get("content-type")?.split(";")[0]?.trim();
158
+ if (!mediaType?.startsWith("video/")) throw new Error("Video service returned a non-video result");
159
+ const artifact = await context.resources.put(new Uint8Array(await response.arrayBuffer()), mediaType);
160
+ return { status: "completed", result: { value: {
161
+ kind: "inline", value: canonicalize(sealGeneratedVideoSet({ videos: [artifact] })),
162
+ } } };
163
+ },
164
+ };
165
+ return defineEndpointPackage({
166
+ module: providerModule, facet: "videos", instance: options.instance, pool: options.pool,
167
+ credentials: { apiKey: options.apiKey }, credentialInputs: { apiKey: { label: "Video service API key" } },
168
+ defaultConcurrency: options.concurrency ?? 1,
169
+ actionLimits: { submit: { concurrency: 1 }, poll: { concurrency: 4 }, collect: { concurrency: 2 } },
170
+ pricing: { kind: "page", url: `${base}/pricing` },
171
+ async readPricing(context) {
172
+ const model = selectWireModelForRequest(mapping, context.request.constraints as unknown as GenerationRequest,
173
+ context.request.pendingInputs?.map((input) => input.input));
174
+ const path = `/rates?model=${encodeURIComponent(model)}`;
175
+ const source = `${base}${path}`;
176
+ const rates = await json(path, key(await context.credentials()));
177
+ return [{ source, data: canonicalize(rates), summary: text(rates.description) }];
178
+ },
179
+ capabilities: [{ capability, returns: generationTypes.videoSet, lifecycle: "asynchronous", supports: serviceSupport, endpoint }],
180
+ });
181
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023", "module": "NodeNext", "moduleResolution": "NodeNext",
4
+ "lib": ["ES2023", "DOM"], "strict": true, "noUncheckedIndexedAccess": true,
5
+ "exactOptionalPropertyTypes": true, "verbatimModuleSyntax": true,
6
+ "declaration": true, "rootDir": "src", "outDir": "dist", "skipLibCheck": true
7
+ },
8
+ "include": ["src/**/*.ts"]
9
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypit/hypit",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "homepage": "https://hypit.ai",
5
5
  "repository": {
6
6
  "type": "git",
@@ -9,6 +9,6 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@hypit/protocol": "workspace:*",
12
- "koffi": "3.2.1"
12
+ "@hypit/file-io-node": "workspace:*"
13
13
  }
14
14
  }
@@ -43,7 +43,7 @@ import {
43
43
  encodeBuildResultManifest,
44
44
  } from "./decode.js";
45
45
  import { syncBuildResultOutputs } from "./writer.js";
46
- import { replaceFile } from "./replace-file.js";
46
+ import { replaceFile } from "@hypit/file-io-node";
47
47
 
48
48
  const manifestName = "result.json";
49
49
  const writerStateName = ".writer.json";
@@ -17,6 +17,6 @@
17
17
  "@hypit/studio-adapter": "workspace:*",
18
18
  "@hypit/fonts-open": "workspace:*",
19
19
  "@hypit/caption": "workspace:*",
20
- "@hypit/timeline": "workspace:*"
20
+ "@hypit/temporal": "workspace:*"
21
21
  }
22
22
  }
@@ -172,7 +172,9 @@ export async function runEnvironmentCommand(input: {
172
172
  }
173
173
 
174
174
  if (args.command === "programs") {
175
- if (runtimeProfile === undefined) throw new Error("programs requires a Runtime Profile");
175
+ if (runtimeProfile === undefined) {
176
+ throw new Error("programs requires a Runtime; run hypit runtime init, select one with runtime use, or pass --runtime <profile>");
177
+ }
176
178
  const profile = resolve(runtimeProfile);
177
179
  const host = await runtimeHost(profile);
178
180
  if (args.action === "up" || args.action === "prepare") {
@@ -195,11 +197,13 @@ export async function runEnvironmentCommand(input: {
195
197
  : await controller.programs.report(args.endpoints === undefined ? {} : { endpoints: args.endpoints });
196
198
  const ready = result.programs.every((item) => item.state.state === "ready");
197
199
  const needsAttention = (item: typeof result.programs[number]) => args.action === "down"
198
- ? item.state.state !== "down" || (item.action !== "stopped" && item.action !== "nothing-to-stop")
200
+ ? item.action !== "nothing-to-stop"
201
+ && (item.state.state !== "down" || item.action !== "stopped")
199
202
  : item.state.state !== "ready";
200
203
  // Readiness describes the service, not whether a stop was performed. An owned process can
201
204
  // still be loading, and another command may have declined a concurrent stop during preparation.
202
205
  const lifecycleOk = args.action === "status" || !result.programs.some(needsAttention);
206
+ const stoppedAny = result.programs.some((item) => item.action === "stopped");
203
207
  const relevant = result.programs.filter((item) => args.presentation.verbose || args.action === "status" || needsAttention(item));
204
208
  const urgent = relevant.filter(needsAttention);
205
209
  const shownPrograms = [...urgent, ...relevant.filter((item) => !needsAttention(item)).slice(0, Math.max(0, args.limit - urgent.length))];
@@ -209,7 +213,8 @@ export async function runEnvironmentCommand(input: {
209
213
  : args.action === "up"
210
214
  ? lifecycleOk ? "External programs ready" : "External programs need attention"
211
215
  : args.action === "down"
212
- ? lifecycleOk ? "External programs stopped" : "External program stop needs attention"
216
+ ? lifecycleOk ? stoppedAny ? "External programs stopped" : "No external programs to stop"
217
+ : "External program stop needs attention"
213
218
  : "External program status";
214
219
  write({
215
220
  format: "hypit.cli-programs@1",
@@ -232,7 +237,9 @@ export async function runEnvironmentCommand(input: {
232
237
  }
233
238
 
234
239
  if (args.command === "runtime") {
235
- if (runtimeProfile === undefined) throw new Error("runtime requires a Runtime Profile");
240
+ if (runtimeProfile === undefined) {
241
+ throw new Error("runtime requires a Runtime; run hypit runtime init, select one with runtime use, or pass --runtime <profile>");
242
+ }
236
243
  const profile = resolve(runtimeProfile);
237
244
  const controller = await runtimeController(profile);
238
245
  if (args.action === "up") {
@@ -298,7 +305,7 @@ export async function runEnvironmentCommand(input: {
298
305
  write({ format: "hypit.cli-runtime-down@1", worker: worker.state },
299
306
  stopped ? "Runtime Worker is down" : "Runtime Worker is still running",
300
307
  stopped ? "success" : "warning", [["Worker", worker.state]],
301
- [`Managed programs were left running. To stop them: ${commandHint(["programs", "down"], { projectRoot, runtimeProfile: resolve(profile) })}`]);
308
+ [`Managed Programs are unchanged. To stop processes started by Hypit: ${commandHint(["programs", "down"], { projectRoot, runtimeProfile: resolve(profile) })}`]);
302
309
  if (!stopped) io.setExitCode?.(1);
303
310
  return;
304
311
  }
@@ -1015,7 +1015,7 @@ export function writeCliHelp(io: CliIo, topic?: string): void {
1015
1015
  row("doctor [profile]", "diagnose selected external setup"),
1016
1016
  row("runtime init|use|unset", "create or select this project's Runtime Profile"),
1017
1017
  row("runtime up|status|logs|down", "prepare and manage the local Build Runtime"),
1018
- row("programs up|status|down", "manage declared external programs only"),
1018
+ row("programs prepare|up|status|down", "manage declared external programs only"),
1019
1019
  row("packages install|status", "manage pinned upstream packages in the machine home"),
1020
1020
  row("activity [--watch]", "show active Builds and their current phases"),
1021
1021
  row("cancel <build-id>", "withdraw one active Build"),