@agentproto/runtime 3.6.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -3
- package/dist/app-ui-build.d.ts +96 -0
- package/dist/app-ui-build.mjs +250 -0
- package/dist/app-ui-build.mjs.map +1 -0
- package/dist/app-ui-delivery.d.ts +75 -0
- package/dist/app-ui-delivery.mjs +144 -0
- package/dist/app-ui-delivery.mjs.map +1 -0
- package/dist/app-ui-placeholder.d.ts +73 -0
- package/dist/app-ui-placeholder.mjs +89 -0
- package/dist/app-ui-placeholder.mjs.map +1 -0
- package/dist/catalog-models.d.ts +5 -3
- package/dist/catalog-models.mjs.map +1 -1
- package/dist/config-schema.d.ts +134 -0
- package/dist/config-schema.mjs +3697 -0
- package/dist/config-schema.mjs.map +1 -0
- package/dist/config.d.ts +22 -2
- package/dist/config.mjs +3615 -4
- package/dist/config.mjs.map +1 -1
- package/dist/{context-continuity-ib9_bVYM.d.ts → context-continuity-Bn99UtVC.d.ts} +40 -1
- package/dist/{heartbeat-COGpMrJS.d.ts → heartbeat-BJtxbDb9.d.ts} +13 -1
- package/dist/heartbeat.d.ts +1 -1
- package/dist/index.d.ts +3057 -151
- package/dist/index.mjs +17669 -7188
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-imports.d.ts +2 -1
- package/dist/resume-strategies.mjs +27 -1
- package/dist/resume-strategies.mjs.map +1 -1
- package/dist/{session-config-DbWP9RRj.d.ts → session-config-ByfVeEQJ.d.ts} +1 -1
- package/dist/session-presence.mjs +3613 -6
- package/dist/session-presence.mjs.map +1 -1
- package/dist/session-story.d.ts +10 -0
- package/dist/{spawn-defaults-CwI9Puwy.d.ts → spawn-defaults-CZj39Znq.d.ts} +146 -2
- package/dist/user-presets.d.ts +8 -2
- package/dist/user-presets.mjs +2 -1
- package/dist/user-presets.mjs.map +1 -1
- package/docs/TRANSMITTER.md +356 -0
- package/docs/mcp-tools/agent_start.md +476 -0
- package/docs/mcp-tools/app_run.md +47 -0
- package/package.json +47 -23
package/README.md
CHANGED
|
@@ -44,6 +44,7 @@ A per-boot bearer token is generated automatically and written into `<workspace>
|
|
|
44
44
|
| Adapter discovery | `GET /adapters` / `POST /adapters/:slug/install` | When `listAgentAdapters` / `installAgentAdapter` is wired |
|
|
45
45
|
| App scope mounts | `POST /apps/:appId/apply` / `DELETE /apps/:appId/apply` / `GET /scopes/:scopeId/apps` | Mirrors MCP `app_apply` / `app_unapply` / `app_list_applied`; needs `appRegistry` |
|
|
46
46
|
| Sessions list | `GET /sessions` / `GET /sessions/:id` / `GET /sessions/summaries` | id-or-name in `:id`; summaries are lightweight + paginated |
|
|
47
|
+
| Session usage | `GET /sessions/:id/usage` | REST twin of `session_usage` MCP tool; add `?includeSubtree=1` to roll up cost + tokens over all descendant sessions |
|
|
47
48
|
| Agent spawn | `POST /sessions/agent` | Long-lived ACP agent (needs `resolveAgentAdapter`) |
|
|
48
49
|
| Interrupt turn | `POST /sessions/:id/interrupt` | Cancel the in-flight turn; session stays alive |
|
|
49
50
|
| Terminal input | `POST /sessions/:id/terminal/input` | Write raw input into a live PTY session |
|
|
@@ -66,16 +67,20 @@ The `/mcp` endpoint exposes the core toolset plus several opt-in / feature-gated
|
|
|
66
67
|
| `session_list` / `session_tree` / `session_usage` / `session_restart` / `session_rename` | Session management |
|
|
67
68
|
| `app_install` / `app_run` / `app_list` / `app_status` / `app_stop` / `app_apply` / `app_unapply` / `app_list_applied` | App-kit apps |
|
|
68
69
|
| `app_data_read` / `app_data_write` / `app_data_list` / `app_data_migrate` | App-scoped durable data plane, anchored at the app's `dataDir` (default `<dir>/data`; `app_install {dataDir}`) |
|
|
69
|
-
| `app_state_append` / `app_state_get` / `app_state_list` | App-scoped **state ledger** (`<
|
|
70
|
+
| `app_state_append` / `app_state_get` / `app_state_list` | App-scoped **state ledger** (`<stateDir>/events.jsonl`, under `~/.agentproto/app-state/`) — append-only, zod-validated event envelope, fold to a stage snapshot; see below |
|
|
70
71
|
| `harness_preset_list` / `harness_preset_create` / `harness_preset_delete` / `harness_preset_set_default` | Persisted harness→auth-profile presets (new) |
|
|
72
|
+
| `review_run` / `review_status` / `review_cancel` / `review_ledger` / `review_export` | [`@agentproto/review`](../review) host: run a REVIEW.md binding over a git range (command lanes as subprocesses, agent lanes as child reviewer sessions), fold a `pass`/`block`/`incomplete` verdict, and record the attestation in the review ledger (`~/.agentproto/reviews`) (new) |
|
|
71
73
|
| `workspace_brain_query` / `workspace_brain_status` / `workspace_brain_ingest` | Per-workspace transcript recall (new) |
|
|
72
74
|
| `conversation_export` | Export a daemon transcript to a target adapter's native store, e.g. `claude-code` (new) |
|
|
73
75
|
| `llm_endpoint_*` (`start`, `stop`, `status`, `set_upstream_link`, `list_links`) | Local LLM Endpoint proxy sidecar — only when `features.llmEndpoint` is enabled (new) |
|
|
74
76
|
|
|
75
77
|
### App state ledger (`app_state_*`)
|
|
76
78
|
|
|
77
|
-
Per installed app, an append-only JSONL event log lives
|
|
78
|
-
|
|
79
|
+
Per installed app, an append-only JSONL event log lives in the daemon's own
|
|
80
|
+
state dir, `~/.agentproto/app-state/<encoded appId>/events.jsonl`
|
|
81
|
+
(`InstalledApp.stateDir`, `src/app-state.ts`) — never in the app's source
|
|
82
|
+
tree. A ledger left at the old `<dataDir>/state/events.jsonl` is still read,
|
|
83
|
+
and moved over on the next append. One JSON object per
|
|
79
84
|
line: `{ id (ULID), ts, appRunId?, stage, item?, kind, by, payload }` with
|
|
80
85
|
`kind ∈ stage-started | gate-report | approval | stage-done | blocked | note`
|
|
81
86
|
and `by ∈ runner | human | policy | system`. Payloads are validated per kind
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { AppUiBuildConfig } from '@agentproto/app-kit';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Build an installed app's UI bundle on demand, per APP.md's declared
|
|
5
|
+
* `ui.build` (`@agentproto/app-kit`'s `AppUiBuildConfig`) — the piece that
|
|
6
|
+
* lets a repo declare "how to build the UI" instead of committing the
|
|
7
|
+
* generated `.agentproto/ui/index.html`.
|
|
8
|
+
*
|
|
9
|
+
* `ensureAppUiBuilt` is the one entry point, called from every path that's
|
|
10
|
+
* about to serve or install an app's UI (`app_install` in app-tools.ts,
|
|
11
|
+
* `GET /apps/:appId/ui` in http-server.ts, the MCP panel cache in
|
|
12
|
+
* app-ui-apps.ts, and the CLI's `agentproto app serve`): if the declared
|
|
13
|
+
* `uiPath` is missing, or older than the newest file matching `ui.build`'s
|
|
14
|
+
* `sources` globs (default `["src/**"]`, resolved against `ui.build.cwd` —
|
|
15
|
+
* default the app dir), it runs `ui.build.command` once, captures output to
|
|
16
|
+
* `<appDir>/.agentproto/ui-build.log`, and returns a result the caller turns
|
|
17
|
+
* into either a served page or a readable error. Concurrent callers for the
|
|
18
|
+
* same `uiPath` share one in-flight build (single-flight, keyed on the
|
|
19
|
+
* absolute `uiPath` — an app's bundle lives at one path regardless of which
|
|
20
|
+
* route is asking).
|
|
21
|
+
*
|
|
22
|
+
* An app with no `ui.build` declared keeps today's behavior exactly: the
|
|
23
|
+
* bundle must already exist on disk, and a missing one is a clear error
|
|
24
|
+
* naming the path rather than a bare 404.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Newest mtime (ms) among every file matched by `patterns` (relative to
|
|
28
|
+
* `cwd`), or `undefined` if nothing matched any pattern. */
|
|
29
|
+
declare function newestSourceMtime(cwd: string, patterns: readonly string[]): Promise<number | undefined>;
|
|
30
|
+
/** Where a `ui.build` run's captured output lands — read this on a build
|
|
31
|
+
* failure for the full log; the returned error carries only a tail. */
|
|
32
|
+
declare function appUiBuildLogPath(appDir: string): string;
|
|
33
|
+
interface EnsureAppUiBuiltInput {
|
|
34
|
+
/** Absolute app directory — anchors a relative `build.cwd` and the build
|
|
35
|
+
* log's location. */
|
|
36
|
+
readonly dir: string;
|
|
37
|
+
/** Absolute path to the ui bundle's entry file (APP.md's `ui.path`,
|
|
38
|
+
* already resolved absolute). Also the single-flight key. */
|
|
39
|
+
readonly uiPath: string;
|
|
40
|
+
readonly build?: AppUiBuildConfig;
|
|
41
|
+
/** Hard timeout for the build command. Defaults to 120s. */
|
|
42
|
+
readonly timeoutMs?: number;
|
|
43
|
+
}
|
|
44
|
+
type EnsureAppUiBuiltResult = {
|
|
45
|
+
readonly ok: true;
|
|
46
|
+
readonly built: boolean;
|
|
47
|
+
} | {
|
|
48
|
+
readonly ok: false;
|
|
49
|
+
readonly error: string;
|
|
50
|
+
};
|
|
51
|
+
/** Peek whether `uiPath` has a build running right now without starting one
|
|
52
|
+
* — a non-blocking caller (`resolveAppUiBuildState`) uses this to join an
|
|
53
|
+
* ALREADY-showing build instead of racing a fresh `ensureAppUiBuilt` call
|
|
54
|
+
* (which would itself re-do the cheap stat/freshness check every time). */
|
|
55
|
+
declare function peekInFlightBuild(uiPath: string): Promise<EnsureAppUiBuiltResult> | undefined;
|
|
56
|
+
/** Paired with {@link peekInFlightBuild}: when that returns a promise, this
|
|
57
|
+
* is the wall-clock time (`Date.now()`) its build command actually started
|
|
58
|
+
* — `undefined` if the in-flight promise hasn't reached `runShellCommand`
|
|
59
|
+
* yet (a handful of stat calls out), in which case "now" is as good a
|
|
60
|
+
* start time as any. */
|
|
61
|
+
declare function peekBuildStartedAt(uiPath: string): number | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* Ensure `uiPath` exists and is at least as new as its declared sources,
|
|
64
|
+
* building it first if not. Single-flight per `uiPath`: a second call for
|
|
65
|
+
* the same path while a build is running awaits the same result instead of
|
|
66
|
+
* starting a second build.
|
|
67
|
+
*/
|
|
68
|
+
declare function ensureAppUiBuilt(input: EnsureAppUiBuiltInput): Promise<EnsureAppUiBuiltResult>;
|
|
69
|
+
type AppUiBuildState = {
|
|
70
|
+
readonly kind: "ready";
|
|
71
|
+
} | {
|
|
72
|
+
readonly kind: "building";
|
|
73
|
+
readonly startedAt: number;
|
|
74
|
+
readonly logTail?: string;
|
|
75
|
+
} | {
|
|
76
|
+
readonly kind: "error";
|
|
77
|
+
readonly message: string;
|
|
78
|
+
readonly logPath: string;
|
|
79
|
+
readonly logTail?: string;
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Non-blocking status check for `ensureAppUiBuilt`'s outcome — the piece
|
|
83
|
+
* that lets a PAGE render (the `ui://app_ui_<id>/view` MCP resource in
|
|
84
|
+
* app-ui-apps.ts, `GET /apps/:appId/ui` in http-server.ts) never stall on a
|
|
85
|
+
* multi-second `ui.build` run. Joins an already-running build via
|
|
86
|
+
* {@link peekInFlightBuild} (or starts one — still single-flight, since
|
|
87
|
+
* `ensureAppUiBuilt` itself owns `inFlight`) and waits only up to
|
|
88
|
+
* `RESOLVE_FAST_PATH_MS`: long enough that the common "already built and
|
|
89
|
+
* fresh" case (a handful of `stat` calls) resolves as `"ready"` without ever
|
|
90
|
+
* showing a placeholder, short enough that a real build never blocks the
|
|
91
|
+
* caller — it keeps running in the background regardless, and the NEXT call
|
|
92
|
+
* (after the page's own reload) picks up wherever that build landed.
|
|
93
|
+
*/
|
|
94
|
+
declare function resolveAppUiBuildState(input: EnsureAppUiBuiltInput): Promise<AppUiBuildState>;
|
|
95
|
+
|
|
96
|
+
export { type AppUiBuildState, type EnsureAppUiBuiltInput, type EnsureAppUiBuiltResult, appUiBuildLogPath, ensureAppUiBuilt, newestSourceMtime, peekBuildStartedAt, peekInFlightBuild, resolveAppUiBuildState };
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { stat, readdir, mkdir, writeFile, readFile } from 'fs/promises';
|
|
3
|
+
import { join, dirname, isAbsolute } from 'path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @agentproto/runtime v0.1.0-alpha
|
|
7
|
+
* Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
function runShellCommand(command, cwd, timeoutMs) {
|
|
11
|
+
return new Promise((resolvePromise) => {
|
|
12
|
+
const child = spawn(command, {
|
|
13
|
+
cwd,
|
|
14
|
+
shell: true,
|
|
15
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
16
|
+
});
|
|
17
|
+
let stdout = "";
|
|
18
|
+
let stderr = "";
|
|
19
|
+
let timedOut = false;
|
|
20
|
+
child.stdout?.on("data", (d) => {
|
|
21
|
+
stdout += d.toString("utf8");
|
|
22
|
+
});
|
|
23
|
+
child.stderr?.on("data", (d) => {
|
|
24
|
+
stderr += d.toString("utf8");
|
|
25
|
+
});
|
|
26
|
+
const timer = setTimeout(() => {
|
|
27
|
+
timedOut = true;
|
|
28
|
+
child.kill("SIGTERM");
|
|
29
|
+
setTimeout(() => child.kill("SIGKILL"), 2e3).unref();
|
|
30
|
+
}, timeoutMs);
|
|
31
|
+
timer.unref();
|
|
32
|
+
child.on("error", (err) => {
|
|
33
|
+
clearTimeout(timer);
|
|
34
|
+
resolvePromise({ exitCode: -1, timedOut, stdout, stderr: stderr + err.message });
|
|
35
|
+
});
|
|
36
|
+
child.on("close", (code) => {
|
|
37
|
+
clearTimeout(timer);
|
|
38
|
+
resolvePromise({ exitCode: code ?? -1, timedOut, stdout, stderr });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function resolveAgainst(dir, path) {
|
|
43
|
+
return isAbsolute(path) ? path : join(dir, path);
|
|
44
|
+
}
|
|
45
|
+
function segmentToRegex(segment) {
|
|
46
|
+
const escaped = segment.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
47
|
+
return new RegExp(`^${escaped}$`);
|
|
48
|
+
}
|
|
49
|
+
async function collectGlobFiles(baseDir, segments) {
|
|
50
|
+
if (segments.length === 0) {
|
|
51
|
+
try {
|
|
52
|
+
const st = await stat(baseDir);
|
|
53
|
+
return st.isFile() ? [baseDir] : [];
|
|
54
|
+
} catch {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const [head, ...rest] = segments;
|
|
59
|
+
if (head === "**") {
|
|
60
|
+
const results2 = [...await collectGlobFiles(baseDir, rest)];
|
|
61
|
+
let entries2;
|
|
62
|
+
try {
|
|
63
|
+
entries2 = await readdir(baseDir, { withFileTypes: true });
|
|
64
|
+
} catch {
|
|
65
|
+
return results2;
|
|
66
|
+
}
|
|
67
|
+
for (const entry of entries2) {
|
|
68
|
+
const full = join(baseDir, entry.name);
|
|
69
|
+
if (entry.isDirectory()) {
|
|
70
|
+
results2.push(...await collectGlobFiles(full, segments));
|
|
71
|
+
} else if (rest.length === 0) {
|
|
72
|
+
results2.push(full);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return results2;
|
|
76
|
+
}
|
|
77
|
+
const regex = segmentToRegex(head);
|
|
78
|
+
let entries;
|
|
79
|
+
try {
|
|
80
|
+
entries = await readdir(baseDir, { withFileTypes: true });
|
|
81
|
+
} catch {
|
|
82
|
+
return [];
|
|
83
|
+
}
|
|
84
|
+
const results = [];
|
|
85
|
+
for (const entry of entries) {
|
|
86
|
+
if (!regex.test(entry.name)) continue;
|
|
87
|
+
results.push(...await collectGlobFiles(join(baseDir, entry.name), rest));
|
|
88
|
+
}
|
|
89
|
+
return results;
|
|
90
|
+
}
|
|
91
|
+
async function newestSourceMtime(cwd, patterns) {
|
|
92
|
+
let max;
|
|
93
|
+
for (const pattern of patterns) {
|
|
94
|
+
const segments = pattern.replace(/^\.\//, "").split("/").filter((s) => s.length > 0);
|
|
95
|
+
if (segments.length === 0) continue;
|
|
96
|
+
for (const file of await collectGlobFiles(cwd, segments)) {
|
|
97
|
+
try {
|
|
98
|
+
const st = await stat(file);
|
|
99
|
+
if (max === void 0 || st.mtimeMs > max) max = st.mtimeMs;
|
|
100
|
+
} catch {
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return max;
|
|
105
|
+
}
|
|
106
|
+
var DEFAULT_BUILD_TIMEOUT_MS = 12e4;
|
|
107
|
+
var DEFAULT_SOURCE_GLOBS = ["src/**"];
|
|
108
|
+
function appUiBuildLogPath(appDir) {
|
|
109
|
+
return join(appDir, ".agentproto", "ui-build.log");
|
|
110
|
+
}
|
|
111
|
+
function tailLines(text, n) {
|
|
112
|
+
const lines = text.split("\n");
|
|
113
|
+
return lines.slice(-40).join("\n");
|
|
114
|
+
}
|
|
115
|
+
var ASSET_REFERENCE_RE = /(?:src|href)\s*=\s*["'](?:\.{0,2}\/)?assets\//i;
|
|
116
|
+
async function warnIfNotSingleFile(uiPath) {
|
|
117
|
+
let html;
|
|
118
|
+
try {
|
|
119
|
+
html = await readFile(uiPath, "utf8");
|
|
120
|
+
} catch {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
if (ASSET_REFERENCE_RE.test(html)) {
|
|
124
|
+
console.warn(
|
|
125
|
+
`[app-ui-build] "${uiPath}" references "./assets" after a ui.build run. The daemon expects a single-file bundle here \u2014 a split build (separate JS/CSS chunks under an assets/ dir) may 404 those chunks unless the app's bundler inlines everything into one file.`
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
130
|
+
var buildStartedAt = /* @__PURE__ */ new Map();
|
|
131
|
+
function peekInFlightBuild(uiPath) {
|
|
132
|
+
return inFlight.get(uiPath);
|
|
133
|
+
}
|
|
134
|
+
function peekBuildStartedAt(uiPath) {
|
|
135
|
+
return buildStartedAt.get(uiPath);
|
|
136
|
+
}
|
|
137
|
+
async function runEnsure(input) {
|
|
138
|
+
const { dir, uiPath, build } = input;
|
|
139
|
+
let existing;
|
|
140
|
+
try {
|
|
141
|
+
existing = await stat(uiPath);
|
|
142
|
+
} catch {
|
|
143
|
+
existing = void 0;
|
|
144
|
+
}
|
|
145
|
+
if (!build) {
|
|
146
|
+
if (existing) return { ok: true, built: false };
|
|
147
|
+
return {
|
|
148
|
+
ok: false,
|
|
149
|
+
error: `app ui bundle "${uiPath}" is missing and the app declares no \`ui.build\` \u2014 either commit the built bundle, or add a \`ui.build\` step to APP.md's \`ui\` frontmatter (\`ui.build.command\`, run with cwd \`ui.build.cwd\` or the app dir).`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const buildCwd = build.cwd ? resolveAgainst(dir, build.cwd) : dir;
|
|
153
|
+
const sources = build.sources ?? DEFAULT_SOURCE_GLOBS;
|
|
154
|
+
if (existing) {
|
|
155
|
+
const newest = await newestSourceMtime(buildCwd, sources);
|
|
156
|
+
if (newest === void 0 || newest <= existing.mtimeMs) {
|
|
157
|
+
return { ok: true, built: false };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const logPath = appUiBuildLogPath(dir);
|
|
161
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
162
|
+
buildStartedAt.set(uiPath, Date.now());
|
|
163
|
+
const result = await runShellCommand(build.command, buildCwd, input.timeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS);
|
|
164
|
+
const logBody = `[${startedAt}] ui.build: ${build.command}
|
|
165
|
+
cwd: ${buildCwd}
|
|
166
|
+
exit: ${result.exitCode}${result.timedOut ? " (timed out)" : ""}
|
|
167
|
+
|
|
168
|
+
--- stdout ---
|
|
169
|
+
${result.stdout}
|
|
170
|
+
--- stderr ---
|
|
171
|
+
${result.stderr}
|
|
172
|
+
`;
|
|
173
|
+
try {
|
|
174
|
+
await mkdir(dirname(logPath), { recursive: true });
|
|
175
|
+
await writeFile(logPath, logBody, "utf8");
|
|
176
|
+
} catch {
|
|
177
|
+
}
|
|
178
|
+
if (result.exitCode !== 0) {
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: `ui.build command "${build.command}" failed (exit ${result.exitCode}${result.timedOut ? ", timed out" : ""}). See "${logPath}" for full output. Last lines:
|
|
182
|
+
${tailLines(result.stderr || result.stdout)}`
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
let rebuilt;
|
|
186
|
+
try {
|
|
187
|
+
rebuilt = await stat(uiPath);
|
|
188
|
+
} catch {
|
|
189
|
+
rebuilt = void 0;
|
|
190
|
+
}
|
|
191
|
+
if (!rebuilt) {
|
|
192
|
+
return {
|
|
193
|
+
ok: false,
|
|
194
|
+
error: `ui.build command "${build.command}" exited 0 but "${uiPath}" is still missing \u2014 check the command actually writes its output there. See "${logPath}" for full output.`
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
await warnIfNotSingleFile(uiPath);
|
|
198
|
+
return { ok: true, built: true };
|
|
199
|
+
}
|
|
200
|
+
async function ensureAppUiBuilt(input) {
|
|
201
|
+
const key = input.uiPath;
|
|
202
|
+
const inflight = inFlight.get(key);
|
|
203
|
+
if (inflight) return inflight;
|
|
204
|
+
const promise = runEnsure(input).finally(() => {
|
|
205
|
+
inFlight.delete(key);
|
|
206
|
+
buildStartedAt.delete(key);
|
|
207
|
+
});
|
|
208
|
+
inFlight.set(key, promise);
|
|
209
|
+
return promise;
|
|
210
|
+
}
|
|
211
|
+
var RESOLVE_FAST_PATH_MS = 300;
|
|
212
|
+
var READABLE_LOG_TAIL_LINES = 20;
|
|
213
|
+
var RESOLVE_TIMEOUT = /* @__PURE__ */ Symbol("app-ui-build-resolve-timeout");
|
|
214
|
+
async function readLogTailSafe(dir, n) {
|
|
215
|
+
try {
|
|
216
|
+
const text = await readFile(appUiBuildLogPath(dir), "utf8");
|
|
217
|
+
const lines = text.split("\n").filter((line) => line.length > 0);
|
|
218
|
+
const tail = lines.slice(-n).join("\n");
|
|
219
|
+
return tail.length > 0 ? tail : void 0;
|
|
220
|
+
} catch {
|
|
221
|
+
return void 0;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async function resolveAppUiBuildState(input) {
|
|
225
|
+
const { dir, uiPath } = input;
|
|
226
|
+
const pending = peekInFlightBuild(uiPath) ?? ensureAppUiBuilt(input);
|
|
227
|
+
const timeout = new Promise((resolve) => {
|
|
228
|
+
const timer = setTimeout(() => resolve(RESOLVE_TIMEOUT), RESOLVE_FAST_PATH_MS);
|
|
229
|
+
timer.unref();
|
|
230
|
+
});
|
|
231
|
+
const raced = await Promise.race([pending, timeout]);
|
|
232
|
+
if (raced === RESOLVE_TIMEOUT) {
|
|
233
|
+
const startedAt = peekBuildStartedAt(uiPath) ?? Date.now();
|
|
234
|
+
return { kind: "building", startedAt, logTail: await readLogTailSafe(dir, READABLE_LOG_TAIL_LINES) };
|
|
235
|
+
}
|
|
236
|
+
const result = raced;
|
|
237
|
+
if (!result.ok) {
|
|
238
|
+
return {
|
|
239
|
+
kind: "error",
|
|
240
|
+
message: result.error,
|
|
241
|
+
logPath: appUiBuildLogPath(dir),
|
|
242
|
+
logTail: await readLogTailSafe(dir, READABLE_LOG_TAIL_LINES)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return { kind: "ready" };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export { appUiBuildLogPath, ensureAppUiBuilt, newestSourceMtime, peekBuildStartedAt, peekInFlightBuild, resolveAppUiBuildState };
|
|
249
|
+
//# sourceMappingURL=app-ui-build.mjs.map
|
|
250
|
+
//# sourceMappingURL=app-ui-build.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/app-ui-build.ts"],"names":["results","entries"],"mappings":"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","file":"app-ui-build.mjs","sourcesContent":["/**\n * Build an installed app's UI bundle on demand, per APP.md's declared\n * `ui.build` (`@agentproto/app-kit`'s `AppUiBuildConfig`) — the piece that\n * lets a repo declare \"how to build the UI\" instead of committing the\n * generated `.agentproto/ui/index.html`.\n *\n * `ensureAppUiBuilt` is the one entry point, called from every path that's\n * about to serve or install an app's UI (`app_install` in app-tools.ts,\n * `GET /apps/:appId/ui` in http-server.ts, the MCP panel cache in\n * app-ui-apps.ts, and the CLI's `agentproto app serve`): if the declared\n * `uiPath` is missing, or older than the newest file matching `ui.build`'s\n * `sources` globs (default `[\"src/**\"]`, resolved against `ui.build.cwd` —\n * default the app dir), it runs `ui.build.command` once, captures output to\n * `<appDir>/.agentproto/ui-build.log`, and returns a result the caller turns\n * into either a served page or a readable error. Concurrent callers for the\n * same `uiPath` share one in-flight build (single-flight, keyed on the\n * absolute `uiPath` — an app's bundle lives at one path regardless of which\n * route is asking).\n *\n * An app with no `ui.build` declared keeps today's behavior exactly: the\n * bundle must already exist on disk, and a missing one is a clear error\n * naming the path rather than a bare 404.\n */\n\nimport { spawn } from \"node:child_process\"\nimport { mkdir, readdir, readFile, stat, writeFile } from \"node:fs/promises\"\nimport type { Dirent, Stats } from \"node:fs\"\nimport { dirname, isAbsolute, join } from \"node:path\"\nimport type { AppUiBuildConfig } from \"@agentproto/app-kit\"\n\n/**\n * Deliberately NOT `command-tools.ts`'s `runCommand` — that pulls in\n * `sessions.ts` (session identity, spawn bookkeeping — thousands of lines\n * unrelated to running a build), which would drag the whole session\n * subsystem into this module's own `tsup` entry (`./app-ui-build`,\n * `tsup.config.ts`), a lean subpath the CLI imports on its own (same\n * pattern as `app-ui-delivery.ts`, which only imports node builtins). A\n * one-shot build command doesn't need `runCommand`'s process-group\n * bookkeeping either — it has no long-lived subtree to reap beyond its own\n * (possibly shell-spawned) child.\n */\ninterface RunResult {\n readonly exitCode: number\n readonly timedOut: boolean\n readonly stdout: string\n readonly stderr: string\n}\n\nfunction runShellCommand(command: string, cwd: string, timeoutMs: number): Promise<RunResult> {\n return new Promise(resolvePromise => {\n const child = spawn(command, {\n cwd,\n shell: true,\n stdio: [\"ignore\", \"pipe\", \"pipe\"],\n })\n let stdout = \"\"\n let stderr = \"\"\n let timedOut = false\n child.stdout?.on(\"data\", d => {\n stdout += d.toString(\"utf8\")\n })\n child.stderr?.on(\"data\", d => {\n stderr += d.toString(\"utf8\")\n })\n const timer = setTimeout(() => {\n timedOut = true\n child.kill(\"SIGTERM\")\n setTimeout(() => child.kill(\"SIGKILL\"), 2_000).unref()\n }, timeoutMs)\n timer.unref()\n child.on(\"error\", err => {\n clearTimeout(timer)\n resolvePromise({ exitCode: -1, timedOut, stdout, stderr: stderr + (err as Error).message })\n })\n child.on(\"close\", code => {\n clearTimeout(timer)\n resolvePromise({ exitCode: code ?? -1, timedOut, stdout, stderr })\n })\n })\n}\n\nfunction resolveAgainst(dir: string, path: string): string {\n return isAbsolute(path) ? path : join(dir, path)\n}\n\n/** Turn one glob SEGMENT (no `/`) into a regex — `*` matches any run of\n * characters within the segment, everything else is literal. */\nfunction segmentToRegex(segment: string): RegExp {\n const escaped = segment.replace(/[.+^${}()|[\\]\\\\]/g, \"\\\\$&\").replace(/\\*/g, \".*\")\n return new RegExp(`^${escaped}$`)\n}\n\n/**\n * Resolve one glob pattern (already split into path segments, relative to\n * `baseDir`) to the absolute paths of every FILE it matches. Supports `**`\n * (zero or more path segments, so `\"src/**\"` matches every file anywhere\n * under `src/`, including `src/` itself) and `*` within a single segment —\n * not full glob syntax (no brace expansion, no negation, no `?`). That\n * subset covers the documented default (`src/**`) and the common overrides\n * (`package.json`, `public/**`, `*.html`) without pulling in a glob\n * dependency for a staleness check.\n */\nasync function collectGlobFiles(baseDir: string, segments: readonly string[]): Promise<string[]> {\n if (segments.length === 0) {\n try {\n const st = await stat(baseDir)\n return st.isFile() ? [baseDir] : []\n } catch {\n return []\n }\n }\n const [head, ...rest] = segments\n if (head === \"**\") {\n const results: string[] = [...(await collectGlobFiles(baseDir, rest))]\n let entries: Dirent[]\n try {\n entries = await readdir(baseDir, { withFileTypes: true })\n } catch {\n return results\n }\n for (const entry of entries) {\n const full = join(baseDir, entry.name)\n if (entry.isDirectory()) {\n results.push(...(await collectGlobFiles(full, segments)))\n } else if (rest.length === 0) {\n results.push(full)\n }\n }\n return results\n }\n const regex = segmentToRegex(head!)\n let entries: Dirent[]\n try {\n entries = await readdir(baseDir, { withFileTypes: true })\n } catch {\n return []\n }\n const results: string[] = []\n for (const entry of entries) {\n if (!regex.test(entry.name)) continue\n results.push(...(await collectGlobFiles(join(baseDir, entry.name), rest)))\n }\n return results\n}\n\n/** Newest mtime (ms) among every file matched by `patterns` (relative to\n * `cwd`), or `undefined` if nothing matched any pattern. */\nexport async function newestSourceMtime(\n cwd: string,\n patterns: readonly string[],\n): Promise<number | undefined> {\n let max: number | undefined\n for (const pattern of patterns) {\n const segments = pattern.replace(/^\\.\\//, \"\").split(\"/\").filter(s => s.length > 0)\n if (segments.length === 0) continue\n for (const file of await collectGlobFiles(cwd, segments)) {\n try {\n const st = await stat(file)\n if (max === undefined || st.mtimeMs > max) max = st.mtimeMs\n } catch {\n // Vanished between listing and stat — ignore.\n }\n }\n }\n return max\n}\n\nconst DEFAULT_BUILD_TIMEOUT_MS = 120_000\nconst DEFAULT_SOURCE_GLOBS = [\"src/**\"]\nconst LOG_TAIL_LINES = 40\n\n/** Where a `ui.build` run's captured output lands — read this on a build\n * failure for the full log; the returned error carries only a tail. */\nexport function appUiBuildLogPath(appDir: string): string {\n return join(appDir, \".agentproto\", \"ui-build.log\")\n}\n\nfunction tailLines(text: string, n: number): string {\n const lines = text.split(\"\\n\")\n return lines.slice(-n).join(\"\\n\")\n}\n\n/** A built single-file bundle should never reference `./assets/…` — the\n * daemon serves `ui.path` standalone (no adjacent static server for a\n * split build's chunks outside the one `assets/` convention `GET\n * /apps/:appId/ui/assets/:file` already covers for a *committed* bundle).\n * A build step that emits a split bundle anyway gets a loud warning, not a\n * failure — the app may still work if its bundler also copied the chunks\n * next to `ui.path`. */\nconst ASSET_REFERENCE_RE = /(?:src|href)\\s*=\\s*[\"'](?:\\.{0,2}\\/)?assets\\//i\n\nasync function warnIfNotSingleFile(uiPath: string): Promise<void> {\n let html: string\n try {\n html = await readFile(uiPath, \"utf8\")\n } catch {\n return\n }\n if (ASSET_REFERENCE_RE.test(html)) {\n console.warn(\n `[app-ui-build] \"${uiPath}\" references \"./assets\" after a ui.build run. ` +\n \"The daemon expects a single-file bundle here — a split build (separate \" +\n \"JS/CSS chunks under an assets/ dir) may 404 those chunks unless the \" +\n \"app's bundler inlines everything into one file.\",\n )\n }\n}\n\nexport interface EnsureAppUiBuiltInput {\n /** Absolute app directory — anchors a relative `build.cwd` and the build\n * log's location. */\n readonly dir: string\n /** Absolute path to the ui bundle's entry file (APP.md's `ui.path`,\n * already resolved absolute). Also the single-flight key. */\n readonly uiPath: string\n readonly build?: AppUiBuildConfig\n /** Hard timeout for the build command. Defaults to 120s. */\n readonly timeoutMs?: number\n}\n\nexport type EnsureAppUiBuiltResult =\n | { readonly ok: true; readonly built: boolean }\n | { readonly ok: false; readonly error: string }\n\n/** In-flight builds keyed on the absolute `uiPath` — an app's bundle lives\n * at one path regardless of which route (app_install, GET .../ui, the MCP\n * panel cache, `app serve`) asked for it, so keying here (rather than on\n * appId, which isn't always known yet — see app-tools.ts's `performInstall`\n * calling this BEFORE the app has an id) still dedupes every caller. */\nconst inFlight = new Map<string, Promise<EnsureAppUiBuiltResult>>()\n\n/** When the CURRENTLY in-flight build (if any) actually started running its\n * `build.command` — set right before `runShellCommand`, read by a\n * non-blocking caller (`resolveAppUiBuildState` below) to render \"building\n * for Ns\" without itself timing anything. Not set for a call that resolves\n * from `existing`/freshness alone (no command ever ran). */\nconst buildStartedAt = new Map<string, number>()\n\n/** Peek whether `uiPath` has a build running right now without starting one\n * — a non-blocking caller (`resolveAppUiBuildState`) uses this to join an\n * ALREADY-showing build instead of racing a fresh `ensureAppUiBuilt` call\n * (which would itself re-do the cheap stat/freshness check every time). */\nexport function peekInFlightBuild(uiPath: string): Promise<EnsureAppUiBuiltResult> | undefined {\n return inFlight.get(uiPath)\n}\n\n/** Paired with {@link peekInFlightBuild}: when that returns a promise, this\n * is the wall-clock time (`Date.now()`) its build command actually started\n * — `undefined` if the in-flight promise hasn't reached `runShellCommand`\n * yet (a handful of stat calls out), in which case \"now\" is as good a\n * start time as any. */\nexport function peekBuildStartedAt(uiPath: string): number | undefined {\n return buildStartedAt.get(uiPath)\n}\n\nasync function runEnsure(input: EnsureAppUiBuiltInput): Promise<EnsureAppUiBuiltResult> {\n const { dir, uiPath, build } = input\n\n let existing: Stats | undefined\n try {\n existing = await stat(uiPath)\n } catch {\n existing = undefined\n }\n\n if (!build) {\n if (existing) return { ok: true, built: false }\n return {\n ok: false,\n error:\n `app ui bundle \"${uiPath}\" is missing and the app declares no \\`ui.build\\` — ` +\n \"either commit the built bundle, or add a `ui.build` step to APP.md's `ui` \" +\n \"frontmatter (`ui.build.command`, run with cwd `ui.build.cwd` or the app dir).\",\n }\n }\n\n const buildCwd = build.cwd ? resolveAgainst(dir, build.cwd) : dir\n const sources = build.sources ?? DEFAULT_SOURCE_GLOBS\n\n if (existing) {\n const newest = await newestSourceMtime(buildCwd, sources)\n if (newest === undefined || newest <= existing.mtimeMs) {\n return { ok: true, built: false }\n }\n }\n\n const logPath = appUiBuildLogPath(dir)\n const startedAt = new Date().toISOString()\n buildStartedAt.set(uiPath, Date.now())\n const result = await runShellCommand(build.command, buildCwd, input.timeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS)\n\n const logBody =\n `[${startedAt}] ui.build: ${build.command}\\n` +\n `cwd: ${buildCwd}\\n` +\n `exit: ${result.exitCode}${result.timedOut ? \" (timed out)\" : \"\"}\\n\\n` +\n `--- stdout ---\\n${result.stdout}\\n--- stderr ---\\n${result.stderr}\\n`\n try {\n await mkdir(dirname(logPath), { recursive: true })\n await writeFile(logPath, logBody, \"utf8\")\n } catch {\n // Best-effort — a log write failure must not hide a build failure.\n }\n\n if (result.exitCode !== 0) {\n return {\n ok: false,\n error:\n `ui.build command \"${build.command}\" failed (exit ${result.exitCode}` +\n `${result.timedOut ? \", timed out\" : \"\"}). See \"${logPath}\" for full output. ` +\n `Last lines:\\n${tailLines(result.stderr || result.stdout, LOG_TAIL_LINES)}`,\n }\n }\n\n let rebuilt: Stats | undefined\n try {\n rebuilt = await stat(uiPath)\n } catch {\n rebuilt = undefined\n }\n if (!rebuilt) {\n return {\n ok: false,\n error:\n `ui.build command \"${build.command}\" exited 0 but \"${uiPath}\" is still missing — ` +\n `check the command actually writes its output there. See \"${logPath}\" for full output.`,\n }\n }\n\n await warnIfNotSingleFile(uiPath)\n\n return { ok: true, built: true }\n}\n\n/**\n * Ensure `uiPath` exists and is at least as new as its declared sources,\n * building it first if not. Single-flight per `uiPath`: a second call for\n * the same path while a build is running awaits the same result instead of\n * starting a second build.\n */\nexport async function ensureAppUiBuilt(\n input: EnsureAppUiBuiltInput,\n): Promise<EnsureAppUiBuiltResult> {\n const key = input.uiPath\n const inflight = inFlight.get(key)\n if (inflight) return inflight\n const promise = runEnsure(input).finally(() => {\n inFlight.delete(key)\n buildStartedAt.delete(key)\n })\n inFlight.set(key, promise)\n return promise\n}\n\nconst RESOLVE_FAST_PATH_MS = 300\nconst READABLE_LOG_TAIL_LINES = 20\nconst RESOLVE_TIMEOUT = Symbol(\"app-ui-build-resolve-timeout\")\n\nasync function readLogTailSafe(dir: string, n: number): Promise<string | undefined> {\n try {\n const text = await readFile(appUiBuildLogPath(dir), \"utf8\")\n const lines = text.split(\"\\n\").filter(line => line.length > 0)\n const tail = lines.slice(-n).join(\"\\n\")\n return tail.length > 0 ? tail : undefined\n } catch {\n return undefined\n }\n}\n\nexport type AppUiBuildState =\n | { readonly kind: \"ready\" }\n | { readonly kind: \"building\"; readonly startedAt: number; readonly logTail?: string }\n | { readonly kind: \"error\"; readonly message: string; readonly logPath: string; readonly logTail?: string }\n\n/**\n * Non-blocking status check for `ensureAppUiBuilt`'s outcome — the piece\n * that lets a PAGE render (the `ui://app_ui_<id>/view` MCP resource in\n * app-ui-apps.ts, `GET /apps/:appId/ui` in http-server.ts) never stall on a\n * multi-second `ui.build` run. Joins an already-running build via\n * {@link peekInFlightBuild} (or starts one — still single-flight, since\n * `ensureAppUiBuilt` itself owns `inFlight`) and waits only up to\n * `RESOLVE_FAST_PATH_MS`: long enough that the common \"already built and\n * fresh\" case (a handful of `stat` calls) resolves as `\"ready\"` without ever\n * showing a placeholder, short enough that a real build never blocks the\n * caller — it keeps running in the background regardless, and the NEXT call\n * (after the page's own reload) picks up wherever that build landed.\n */\nexport async function resolveAppUiBuildState(input: EnsureAppUiBuiltInput): Promise<AppUiBuildState> {\n const { dir, uiPath } = input\n const pending = peekInFlightBuild(uiPath) ?? ensureAppUiBuilt(input)\n\n const timeout = new Promise<typeof RESOLVE_TIMEOUT>(resolve => {\n const timer = setTimeout(() => resolve(RESOLVE_TIMEOUT), RESOLVE_FAST_PATH_MS)\n timer.unref()\n })\n const raced = await Promise.race([pending, timeout])\n\n if (raced === RESOLVE_TIMEOUT) {\n const startedAt = peekBuildStartedAt(uiPath) ?? Date.now()\n return { kind: \"building\", startedAt, logTail: await readLogTailSafe(dir, READABLE_LOG_TAIL_LINES) }\n }\n const result = raced as EnsureAppUiBuiltResult\n if (!result.ok) {\n return {\n kind: \"error\",\n message: result.error,\n logPath: appUiBuildLogPath(dir),\n logTail: await readLogTailSafe(dir, READABLE_LOG_TAIL_LINES),\n }\n }\n return { kind: \"ready\" }\n}\n"]}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HTTP delivery primitives for the app-UI shell — `GET /apps/:appId/ui` and
|
|
5
|
+
* `GET /apps/:appId/ui/assets/:file` on the daemon (http-server.ts), and the
|
|
6
|
+
* same shapes under `agentproto app serve` (cli/src/app-serve.ts). One
|
|
7
|
+
* module so both hosts negotiate, validate and cache identically:
|
|
8
|
+
*
|
|
9
|
+
* - `accept-encoding` negotiation (`br` preferred, then `gzip`, else
|
|
10
|
+
* identity) with `vary: accept-encoding` on every response.
|
|
11
|
+
* - A strong `etag` (sha256 of the exact bytes served) answered with 304
|
|
12
|
+
* on a matching `if-none-match`.
|
|
13
|
+
* - An LRU of encoded representations keyed by a caller-chosen key and
|
|
14
|
+
* invalidated by a caller-chosen stamp (path + mtime + size for files),
|
|
15
|
+
* so neither the digest nor the compression reruns until the file
|
|
16
|
+
* changes.
|
|
17
|
+
*
|
|
18
|
+
* Deliberately scoped to the shell: SSE, MCP and every API route keep their
|
|
19
|
+
* own framing and never go through here.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
type ContentCoding = "br" | "gzip" | "identity";
|
|
23
|
+
/** Pick the response coding from an `accept-encoding` header: `br` when
|
|
24
|
+
* acceptable, then `gzip`, else identity. q-values are honoured only as
|
|
25
|
+
* acceptable (`q>0`) vs refused (`q=0`) — the preference order is ours, not
|
|
26
|
+
* the client's. A `*` entry covers codings the header doesn't name. */
|
|
27
|
+
declare function negotiateContentEncoding(header: string | string[] | undefined): ContentCoding;
|
|
28
|
+
/** Strong entity tag over `body` (sha256, base64url). `variant`, when set,
|
|
29
|
+
* is appended so two representations of the same bytes that carry
|
|
30
|
+
* different response headers (e.g. with/without frame headers) never
|
|
31
|
+
* validate each other's cache entry. */
|
|
32
|
+
declare function strongEtag(body: Buffer | string, variant?: string): string;
|
|
33
|
+
/** `if-none-match` evaluation (RFC 9110 §13.1.2): `*` or any listed tag
|
|
34
|
+
* equal to `etag` under weak comparison. */
|
|
35
|
+
declare function ifNoneMatchHits(header: string | string[] | undefined, etag: string): boolean;
|
|
36
|
+
/** Content types the app-UI routes serve, by lowercase extension. */
|
|
37
|
+
declare const APP_UI_MIME_BY_EXT: Readonly<Record<string, string>>;
|
|
38
|
+
/** Content type for an extension (with or without the leading dot). */
|
|
39
|
+
declare function appUiContentType(ext: string): string;
|
|
40
|
+
/** Whether a content type is worth compressing — text-ish only; woff2/png
|
|
41
|
+
* and friends are already compressed. */
|
|
42
|
+
declare function isCompressibleContentType(contentType: string): boolean;
|
|
43
|
+
/** `:file` segment of the assets route: a flat name from `[A-Za-z0-9._-]`,
|
|
44
|
+
* not starting with `.` (so no `.`/`..`, no dotfiles), no separators. */
|
|
45
|
+
declare function isValidAppUiAssetName(file: string): boolean;
|
|
46
|
+
/** Assets route cache policy — Vite emits content-hashed file names. */
|
|
47
|
+
declare const IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
48
|
+
/** One servable body, its strong etag, and its encodings (computed lazily,
|
|
49
|
+
* memoized — a representation that is never requested as gzip never pays
|
|
50
|
+
* for it). */
|
|
51
|
+
interface EncodedRepresentation {
|
|
52
|
+
readonly etag: string;
|
|
53
|
+
readonly compressible: boolean;
|
|
54
|
+
body(coding: ContentCoding): Buffer;
|
|
55
|
+
}
|
|
56
|
+
declare function createEncodedRepresentation(identity: Buffer, opts: {
|
|
57
|
+
compressible: boolean;
|
|
58
|
+
etag?: string;
|
|
59
|
+
}): EncodedRepresentation;
|
|
60
|
+
/** Small LRU of representations. `stamp` is whatever identifies the source
|
|
61
|
+
* version (e.g. `${mtimeMs}:${size}`); a stamp mismatch rebuilds. Bounded
|
|
62
|
+
* because some keys fold in request-derived input (the injected base URL). */
|
|
63
|
+
interface RepresentationCache {
|
|
64
|
+
get(key: string, stamp: string, build: () => Promise<EncodedRepresentation> | EncodedRepresentation): Promise<EncodedRepresentation>;
|
|
65
|
+
readonly size: number;
|
|
66
|
+
}
|
|
67
|
+
declare function createRepresentationCache(maxEntries?: number): RepresentationCache;
|
|
68
|
+
/** Write `rep` to `res` with `headers` (content-type, cache-control, frame
|
|
69
|
+
* headers — whatever the route decided), adding `etag`, `vary:
|
|
70
|
+
* accept-encoding`, `content-encoding` and `content-length`. A matching
|
|
71
|
+
* `if-none-match` answers 304 with the same headers and no body, so a
|
|
72
|
+
* revalidation refreshes the stored headers exactly as a 200 would. */
|
|
73
|
+
declare function sendRepresentation(req: IncomingMessage, res: ServerResponse, rep: EncodedRepresentation, headers: Record<string, string>): void;
|
|
74
|
+
|
|
75
|
+
export { APP_UI_MIME_BY_EXT, type ContentCoding, type EncodedRepresentation, IMMUTABLE_CACHE_CONTROL, type RepresentationCache, appUiContentType, createEncodedRepresentation, createRepresentationCache, ifNoneMatchHits, isCompressibleContentType, isValidAppUiAssetName, negotiateContentEncoding, sendRepresentation, strongEtag };
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
2
|
+
import { brotliCompressSync, constants, gzipSync } from 'zlib';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @agentproto/runtime v0.1.0-alpha
|
|
6
|
+
* Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function negotiateContentEncoding(header) {
|
|
10
|
+
const raw = Array.isArray(header) ? header.join(",") : header;
|
|
11
|
+
if (!raw) return "identity";
|
|
12
|
+
const q = /* @__PURE__ */ new Map();
|
|
13
|
+
for (const part of raw.split(",")) {
|
|
14
|
+
const [name, ...params] = part.trim().toLowerCase().split(";");
|
|
15
|
+
if (!name) continue;
|
|
16
|
+
let weight = 1;
|
|
17
|
+
for (const p of params) {
|
|
18
|
+
const m = p.trim().match(/^q=([0-9.]+)$/);
|
|
19
|
+
if (m) weight = Number(m[1]);
|
|
20
|
+
}
|
|
21
|
+
q.set(name === "x-gzip" ? "gzip" : name, Number.isFinite(weight) ? weight : 0);
|
|
22
|
+
}
|
|
23
|
+
const acceptable = (coding) => {
|
|
24
|
+
const v = q.get(coding) ?? q.get("*");
|
|
25
|
+
return v !== void 0 && v > 0;
|
|
26
|
+
};
|
|
27
|
+
if (acceptable("br")) return "br";
|
|
28
|
+
if (acceptable("gzip")) return "gzip";
|
|
29
|
+
return "identity";
|
|
30
|
+
}
|
|
31
|
+
function strongEtag(body, variant) {
|
|
32
|
+
const digest = createHash("sha256").update(body).digest("base64url");
|
|
33
|
+
return variant ? `"${digest}-${variant}"` : `"${digest}"`;
|
|
34
|
+
}
|
|
35
|
+
function ifNoneMatchHits(header, etag) {
|
|
36
|
+
const raw = Array.isArray(header) ? header.join(",") : header;
|
|
37
|
+
if (!raw) return false;
|
|
38
|
+
const bare = etag.replace(/^W\//, "");
|
|
39
|
+
for (const part of raw.split(",")) {
|
|
40
|
+
const tag = part.trim();
|
|
41
|
+
if (tag === "*") return true;
|
|
42
|
+
if (tag.replace(/^W\//, "") === bare) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
var APP_UI_MIME_BY_EXT = {
|
|
47
|
+
".html": "text/html; charset=utf-8",
|
|
48
|
+
".htm": "text/html; charset=utf-8",
|
|
49
|
+
".js": "text/javascript; charset=utf-8",
|
|
50
|
+
".mjs": "text/javascript; charset=utf-8",
|
|
51
|
+
".css": "text/css; charset=utf-8",
|
|
52
|
+
".json": "application/json; charset=utf-8",
|
|
53
|
+
".webmanifest": "application/manifest+json; charset=utf-8",
|
|
54
|
+
".map": "application/json; charset=utf-8",
|
|
55
|
+
".svg": "image/svg+xml",
|
|
56
|
+
".png": "image/png",
|
|
57
|
+
".jpg": "image/jpeg",
|
|
58
|
+
".jpeg": "image/jpeg",
|
|
59
|
+
".gif": "image/gif",
|
|
60
|
+
".webp": "image/webp",
|
|
61
|
+
".ico": "image/x-icon",
|
|
62
|
+
".woff": "font/woff",
|
|
63
|
+
".woff2": "font/woff2",
|
|
64
|
+
".ttf": "font/ttf",
|
|
65
|
+
".txt": "text/plain; charset=utf-8",
|
|
66
|
+
".wasm": "application/wasm"
|
|
67
|
+
};
|
|
68
|
+
function appUiContentType(ext) {
|
|
69
|
+
const key = (ext.startsWith(".") ? ext : `.${ext}`).toLowerCase();
|
|
70
|
+
return APP_UI_MIME_BY_EXT[key] ?? "application/octet-stream";
|
|
71
|
+
}
|
|
72
|
+
function isCompressibleContentType(contentType) {
|
|
73
|
+
const base = contentType.split(";")[0].trim().toLowerCase();
|
|
74
|
+
return base.startsWith("text/") || base === "application/json" || base === "application/javascript" || base === "image/svg+xml" || base === "application/wasm";
|
|
75
|
+
}
|
|
76
|
+
function isValidAppUiAssetName(file) {
|
|
77
|
+
return /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(file);
|
|
78
|
+
}
|
|
79
|
+
var IMMUTABLE_CACHE_CONTROL = "public, max-age=31536000, immutable";
|
|
80
|
+
function createEncodedRepresentation(identity, opts) {
|
|
81
|
+
const encoded = /* @__PURE__ */ new Map([["identity", identity]]);
|
|
82
|
+
return {
|
|
83
|
+
etag: opts.etag ?? strongEtag(identity),
|
|
84
|
+
compressible: opts.compressible,
|
|
85
|
+
body(coding) {
|
|
86
|
+
const hit = encoded.get(coding);
|
|
87
|
+
if (hit) return hit;
|
|
88
|
+
const out = coding === "br" ? brotliCompressSync(identity, {
|
|
89
|
+
params: {
|
|
90
|
+
[constants.BROTLI_PARAM_QUALITY]: 5,
|
|
91
|
+
[constants.BROTLI_PARAM_SIZE_HINT]: identity.length
|
|
92
|
+
}
|
|
93
|
+
}) : gzipSync(identity, { level: 6 });
|
|
94
|
+
encoded.set(coding, out);
|
|
95
|
+
return out;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function createRepresentationCache(maxEntries = 64) {
|
|
100
|
+
const entries = /* @__PURE__ */ new Map();
|
|
101
|
+
return {
|
|
102
|
+
async get(key, stamp, build) {
|
|
103
|
+
const hit = entries.get(key);
|
|
104
|
+
if (hit && hit.stamp === stamp) {
|
|
105
|
+
entries.delete(key);
|
|
106
|
+
entries.set(key, hit);
|
|
107
|
+
return hit.rep;
|
|
108
|
+
}
|
|
109
|
+
const rep = await build();
|
|
110
|
+
entries.delete(key);
|
|
111
|
+
entries.set(key, { stamp, rep });
|
|
112
|
+
while (entries.size > maxEntries) {
|
|
113
|
+
const oldest = entries.keys().next().value;
|
|
114
|
+
if (oldest === void 0) break;
|
|
115
|
+
entries.delete(oldest);
|
|
116
|
+
}
|
|
117
|
+
return rep;
|
|
118
|
+
},
|
|
119
|
+
get size() {
|
|
120
|
+
return entries.size;
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function sendRepresentation(req, res, rep, headers) {
|
|
125
|
+
const prior = res.getHeader("vary");
|
|
126
|
+
const priorVary = Array.isArray(prior) ? prior.join(", ") : prior === void 0 ? "" : String(prior);
|
|
127
|
+
const vary = /(^|,)\s*accept-encoding\s*(,|$)/i.test(priorVary) ? priorVary : priorVary ? `${priorVary}, accept-encoding` : "accept-encoding";
|
|
128
|
+
const out = { ...headers, etag: rep.etag, vary };
|
|
129
|
+
if (ifNoneMatchHits(req.headers["if-none-match"], rep.etag)) {
|
|
130
|
+
res.writeHead(304, out);
|
|
131
|
+
res.end();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const coding = rep.compressible ? negotiateContentEncoding(req.headers["accept-encoding"]) : "identity";
|
|
135
|
+
const body = rep.body(coding);
|
|
136
|
+
if (coding !== "identity") out["content-encoding"] = coding;
|
|
137
|
+
out["content-length"] = String(body.length);
|
|
138
|
+
res.writeHead(200, out);
|
|
139
|
+
res.end(req.method === "HEAD" ? void 0 : body);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export { APP_UI_MIME_BY_EXT, IMMUTABLE_CACHE_CONTROL, appUiContentType, createEncodedRepresentation, createRepresentationCache, ifNoneMatchHits, isCompressibleContentType, isValidAppUiAssetName, negotiateContentEncoding, sendRepresentation, strongEtag };
|
|
143
|
+
//# sourceMappingURL=app-ui-delivery.mjs.map
|
|
144
|
+
//# sourceMappingURL=app-ui-delivery.mjs.map
|