chapterhouse 0.1.1
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/LICENSE +23 -0
- package/README.md +363 -0
- package/agents/chapterhouse.agent.md +40 -0
- package/agents/coder.agent.md +38 -0
- package/agents/designer.agent.md +43 -0
- package/agents/general-purpose.agent.md +30 -0
- package/dist/api/auth.js +159 -0
- package/dist/api/auth.test.js +463 -0
- package/dist/api/errors.js +95 -0
- package/dist/api/errors.test.js +89 -0
- package/dist/api/rate-limit.js +85 -0
- package/dist/api/server-runtime.js +62 -0
- package/dist/api/server.js +651 -0
- package/dist/api/server.test.js +385 -0
- package/dist/api/sse.integration.test.js +270 -0
- package/dist/api/sse.js +7 -0
- package/dist/api/team.js +196 -0
- package/dist/api/team.test.js +466 -0
- package/dist/cli.js +102 -0
- package/dist/config.js +299 -0
- package/dist/config.phase3.test.js +20 -0
- package/dist/config.test.js +148 -0
- package/dist/copilot/agents.js +447 -0
- package/dist/copilot/agents.squad.test.js +72 -0
- package/dist/copilot/classifier.js +72 -0
- package/dist/copilot/client.js +32 -0
- package/dist/copilot/client.test.js +100 -0
- package/dist/copilot/episode-writer.js +219 -0
- package/dist/copilot/episode-writer.test.js +41 -0
- package/dist/copilot/mcp-config.js +22 -0
- package/dist/copilot/okr-mapper.js +196 -0
- package/dist/copilot/okr-mapper.test.js +114 -0
- package/dist/copilot/orchestrator.js +685 -0
- package/dist/copilot/orchestrator.test.js +523 -0
- package/dist/copilot/router.js +142 -0
- package/dist/copilot/router.test.js +119 -0
- package/dist/copilot/skills.js +125 -0
- package/dist/copilot/standup.js +138 -0
- package/dist/copilot/standup.test.js +132 -0
- package/dist/copilot/system-message.js +143 -0
- package/dist/copilot/system-message.test.js +17 -0
- package/dist/copilot/tools.js +1212 -0
- package/dist/copilot/tools.okr.test.js +260 -0
- package/dist/copilot/tools.squad.test.js +168 -0
- package/dist/daemon.js +235 -0
- package/dist/home-path.js +12 -0
- package/dist/home-path.test.js +11 -0
- package/dist/integrations/ado-analytics.js +178 -0
- package/dist/integrations/ado-analytics.test.js +284 -0
- package/dist/integrations/ado-client.js +227 -0
- package/dist/integrations/ado-client.test.js +176 -0
- package/dist/integrations/ado-schema.js +25 -0
- package/dist/integrations/ado-schema.test.js +39 -0
- package/dist/integrations/ado-skill.js +55 -0
- package/dist/integrations/report-generator.js +114 -0
- package/dist/integrations/report-generator.test.js +62 -0
- package/dist/integrations/team-push.js +144 -0
- package/dist/integrations/team-push.test.js +178 -0
- package/dist/integrations/teams-notify.js +108 -0
- package/dist/integrations/teams-notify.test.js +135 -0
- package/dist/paths.js +41 -0
- package/dist/setup.js +149 -0
- package/dist/shutdown-signals.js +13 -0
- package/dist/shutdown-signals.test.js +33 -0
- package/dist/squad/charter.js +108 -0
- package/dist/squad/charter.test.js +89 -0
- package/dist/squad/context.js +48 -0
- package/dist/squad/context.test.js +59 -0
- package/dist/squad/discovery.js +280 -0
- package/dist/squad/discovery.test.js +93 -0
- package/dist/squad/index.js +7 -0
- package/dist/squad/mirror.js +81 -0
- package/dist/squad/mirror.scheduler.js +78 -0
- package/dist/squad/mirror.scheduler.test.js +197 -0
- package/dist/squad/mirror.test.js +172 -0
- package/dist/squad/registry.js +162 -0
- package/dist/squad/registry.test.js +31 -0
- package/dist/squad/squad-coordinator-system-message.test.js +190 -0
- package/dist/squad/squad-session-routing.test.js +260 -0
- package/dist/squad/types.js +4 -0
- package/dist/status.js +25 -0
- package/dist/status.test.js +22 -0
- package/dist/store/db.js +290 -0
- package/dist/store/db.test.js +126 -0
- package/dist/store/squad-sessions.test.js +341 -0
- package/dist/test/setup-env.js +3 -0
- package/dist/update.js +112 -0
- package/dist/update.test.js +25 -0
- package/dist/wiki/context.js +138 -0
- package/dist/wiki/fs.js +195 -0
- package/dist/wiki/fs.test.js +39 -0
- package/dist/wiki/index-manager.js +359 -0
- package/dist/wiki/index-manager.test.js +129 -0
- package/dist/wiki/lock.js +26 -0
- package/dist/wiki/lock.test.js +30 -0
- package/dist/wiki/log-manager.js +20 -0
- package/dist/wiki/migrate.js +306 -0
- package/dist/wiki/okr.test.js +101 -0
- package/dist/wiki/path-utils.js +4 -0
- package/dist/wiki/path-utils.test.js +8 -0
- package/dist/wiki/seed-team-wiki.js +296 -0
- package/dist/wiki/seed-team-wiki.test.js +69 -0
- package/dist/wiki/team-sync.js +212 -0
- package/dist/wiki/team-sync.test.js +185 -0
- package/dist/wiki/templates/okr.js +98 -0
- package/package.json +72 -0
- package/skills/.gitkeep +0 -0
- package/skills/find-skills/SKILL.md +161 -0
- package/skills/find-skills/_meta.json +4 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +42 -0
- package/skills/squad/SKILL.md +76 -0
- package/web/dist/assets/index-D-e7K-fT.css +10 -0
- package/web/dist/assets/index-DAg9IrpO.js +142 -0
- package/web/dist/assets/index-DAg9IrpO.js.map +1 -0
- package/web/dist/chapterhouse-icon.png +0 -0
- package/web/dist/chapterhouse-icon.svg +42 -0
- package/web/dist/chapterhouse-logo.svg +46 -0
- package/web/dist/index.html +15 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ZodError } from "zod";
|
|
2
|
+
export class HttpError extends Error {
|
|
3
|
+
statusCode;
|
|
4
|
+
expose;
|
|
5
|
+
constructor(statusCode, message, expose = statusCode < 500) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.statusCode = statusCode;
|
|
8
|
+
this.expose = expose;
|
|
9
|
+
this.name = new.target.name;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export class BadRequestError extends HttpError {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(400, message);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export class UnauthorizedError extends HttpError {
|
|
18
|
+
constructor(message = "Unauthorized") {
|
|
19
|
+
super(401, message);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export class ForbiddenError extends HttpError {
|
|
23
|
+
constructor(message = "Forbidden") {
|
|
24
|
+
super(403, message);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export class NotFoundError extends HttpError {
|
|
28
|
+
constructor(message = "Not found") {
|
|
29
|
+
super(404, message);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export class InternalServerError extends HttpError {
|
|
33
|
+
constructor(message = "Internal server error", expose = false) {
|
|
34
|
+
super(500, message, expose);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function isBodyParserSyntaxError(error) {
|
|
38
|
+
if (!(error instanceof SyntaxError)) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
const candidate = error;
|
|
42
|
+
return candidate.status === 400 && candidate.type === "entity.parse.failed";
|
|
43
|
+
}
|
|
44
|
+
function formatZodError(error) {
|
|
45
|
+
return error.issues[0]?.message ?? "Invalid request";
|
|
46
|
+
}
|
|
47
|
+
export function parseRequest(schema, input) {
|
|
48
|
+
const parsed = schema.safeParse(input);
|
|
49
|
+
if (!parsed.success) {
|
|
50
|
+
throw new BadRequestError(formatZodError(parsed.error));
|
|
51
|
+
}
|
|
52
|
+
return parsed.data;
|
|
53
|
+
}
|
|
54
|
+
export function asBadRequest(error) {
|
|
55
|
+
if (error instanceof HttpError) {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
throw new BadRequestError(error instanceof Error ? error.message : "Invalid request");
|
|
59
|
+
}
|
|
60
|
+
export function apiNotFoundHandler(req, _res, next) {
|
|
61
|
+
if (req.path === "/stream" || req.path.startsWith("/api/")) {
|
|
62
|
+
next(new NotFoundError("Not found"));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
next();
|
|
66
|
+
}
|
|
67
|
+
export function createApiErrorHandler() {
|
|
68
|
+
return (error, req, res, next) => {
|
|
69
|
+
if (res.headersSent) {
|
|
70
|
+
next(error);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (isBodyParserSyntaxError(error)) {
|
|
74
|
+
res.status(400).json({ error: "Invalid JSON request body" });
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (error instanceof ZodError) {
|
|
78
|
+
res.status(400).json({ error: formatZodError(error) });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (error instanceof HttpError) {
|
|
82
|
+
if (error.statusCode >= 500) {
|
|
83
|
+
console.error(`[api] ${req.method} ${req.originalUrl} failed:`, error);
|
|
84
|
+
}
|
|
85
|
+
else if (error.statusCode === 401 || error.statusCode === 403) {
|
|
86
|
+
console.warn(`[api] ${req.method} ${req.originalUrl} denied: ${error.message}`);
|
|
87
|
+
}
|
|
88
|
+
res.status(error.statusCode).json({ error: error.expose ? error.message : "Internal server error" });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
console.error(`[api] ${req.method} ${req.originalUrl} failed:`, error);
|
|
92
|
+
res.status(500).json({ error: "Internal server error" });
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { createServer } from "node:http";
|
|
4
|
+
import express from "express";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { BadRequestError, InternalServerError, NotFoundError, apiNotFoundHandler, createApiErrorHandler, parseRequest } from "./errors.js";
|
|
7
|
+
async function withServer(app, run) {
|
|
8
|
+
const server = createServer(app);
|
|
9
|
+
await new Promise((resolve) => {
|
|
10
|
+
server.listen(0, "127.0.0.1", () => resolve());
|
|
11
|
+
});
|
|
12
|
+
const address = server.address();
|
|
13
|
+
assert.ok(address && typeof address === "object", "server should expose a bound address");
|
|
14
|
+
try {
|
|
15
|
+
await run(`http://127.0.0.1:${address.port}`);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
server.close((err) => (err ? reject(err) : resolve()));
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
test("centralized error handler returns JSON for invalid JSON bodies", async () => {
|
|
24
|
+
const app = express();
|
|
25
|
+
app.use(express.json());
|
|
26
|
+
app.post("/api/test", (_req, res) => {
|
|
27
|
+
res.json({ ok: true });
|
|
28
|
+
});
|
|
29
|
+
app.use(createApiErrorHandler());
|
|
30
|
+
await withServer(app, async (baseUrl) => {
|
|
31
|
+
const response = await fetch(`${baseUrl}/api/test`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "content-type": "application/json" },
|
|
34
|
+
body: "{bad json",
|
|
35
|
+
});
|
|
36
|
+
assert.equal(response.status, 400);
|
|
37
|
+
assert.deepEqual(await response.json(), { error: "Invalid JSON request body" });
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
test("centralized error handler returns HTTP errors as JSON", async () => {
|
|
41
|
+
const app = express();
|
|
42
|
+
app.get("/api/missing", () => {
|
|
43
|
+
throw new NotFoundError("Task not found");
|
|
44
|
+
});
|
|
45
|
+
app.use(createApiErrorHandler());
|
|
46
|
+
await withServer(app, async (baseUrl) => {
|
|
47
|
+
const response = await fetch(`${baseUrl}/api/missing`);
|
|
48
|
+
assert.equal(response.status, 404);
|
|
49
|
+
assert.deepEqual(await response.json(), { error: "Task not found" });
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
test("parseRequest converts Zod failures into bad-request errors", () => {
|
|
53
|
+
assert.throws(() => parseRequest(z.object({ prompt: z.string({ error: "Missing 'prompt' in request body" }).min(1, "Missing 'prompt' in request body") }), {}), (error) => error instanceof BadRequestError && error.message === "Missing 'prompt' in request body");
|
|
54
|
+
});
|
|
55
|
+
test("api not-found handler returns JSON for unknown API routes", async () => {
|
|
56
|
+
const app = express();
|
|
57
|
+
app.use(apiNotFoundHandler);
|
|
58
|
+
app.use(createApiErrorHandler());
|
|
59
|
+
await withServer(app, async (baseUrl) => {
|
|
60
|
+
const response = await fetch(`${baseUrl}/api/unknown`);
|
|
61
|
+
assert.equal(response.status, 404);
|
|
62
|
+
assert.deepEqual(await response.json(), { error: "Not found" });
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
test("centralized error handler hides internal error details", async () => {
|
|
66
|
+
const app = express();
|
|
67
|
+
const originalConsoleError = console.error;
|
|
68
|
+
const logged = [];
|
|
69
|
+
console.error = (...args) => {
|
|
70
|
+
logged.push(args);
|
|
71
|
+
};
|
|
72
|
+
try {
|
|
73
|
+
app.get("/api/failure", () => {
|
|
74
|
+
throw new InternalServerError("do not leak", false);
|
|
75
|
+
});
|
|
76
|
+
app.use(createApiErrorHandler());
|
|
77
|
+
await withServer(app, async (baseUrl) => {
|
|
78
|
+
const response = await fetch(`${baseUrl}/api/failure`);
|
|
79
|
+
assert.equal(response.status, 500);
|
|
80
|
+
assert.deepEqual(await response.json(), { error: "Internal server error" });
|
|
81
|
+
});
|
|
82
|
+
assert.equal(logged.length, 1);
|
|
83
|
+
assert.equal(logged[0]?.[0], "[api] GET /api/failure failed:");
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
console.error = originalConsoleError;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
//# sourceMappingURL=errors.test.js.map
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export function getClientIp(req) {
|
|
2
|
+
const ip = typeof req.ip === "string" && req.ip.trim().length > 0
|
|
3
|
+
? req.ip.trim()
|
|
4
|
+
: req.socket.remoteAddress?.trim() || "unknown";
|
|
5
|
+
return ip.startsWith("::ffff:") ? ip.slice(7) : ip;
|
|
6
|
+
}
|
|
7
|
+
function writeRateLimitResponse(req, res, retryAfterSeconds, fallbackMessage, onLimit) {
|
|
8
|
+
if (onLimit) {
|
|
9
|
+
onLimit(req, res, retryAfterSeconds);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
res.setHeader("Retry-After", String(retryAfterSeconds));
|
|
13
|
+
res.status(429).json({ error: fallbackMessage.replace("{retryAfterSeconds}", String(retryAfterSeconds)) });
|
|
14
|
+
}
|
|
15
|
+
export function createFixedWindowRateLimiter(options) {
|
|
16
|
+
const buckets = new Map();
|
|
17
|
+
const keyGenerator = options.keyGenerator ?? getClientIp;
|
|
18
|
+
const now = options.now ?? (() => Date.now());
|
|
19
|
+
let seenRequests = 0;
|
|
20
|
+
function pruneExpiredBuckets(currentTime) {
|
|
21
|
+
for (const [key, bucket] of buckets) {
|
|
22
|
+
if (bucket.resetAt <= currentTime) {
|
|
23
|
+
buckets.delete(key);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return function fixedWindowRateLimiter(req, res, next) {
|
|
28
|
+
if (options.skip?.(req)) {
|
|
29
|
+
next();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const currentTime = now();
|
|
33
|
+
const key = keyGenerator(req);
|
|
34
|
+
const currentBucket = buckets.get(key);
|
|
35
|
+
const bucket = !currentBucket || currentBucket.resetAt <= currentTime
|
|
36
|
+
? { count: 0, resetAt: currentTime + options.windowMs }
|
|
37
|
+
: currentBucket;
|
|
38
|
+
bucket.count += 1;
|
|
39
|
+
buckets.set(key, bucket);
|
|
40
|
+
const retryAfterSeconds = Math.max(1, Math.ceil((bucket.resetAt - currentTime) / 1000));
|
|
41
|
+
res.setHeader("X-RateLimit-Limit", String(options.maxRequests));
|
|
42
|
+
res.setHeader("X-RateLimit-Remaining", String(Math.max(0, options.maxRequests - bucket.count)));
|
|
43
|
+
res.setHeader("X-RateLimit-Reset", String(Math.ceil(bucket.resetAt / 1000)));
|
|
44
|
+
if (bucket.count > options.maxRequests) {
|
|
45
|
+
writeRateLimitResponse(req, res, retryAfterSeconds, "Too many requests. Retry after {retryAfterSeconds} seconds.", options.onLimit);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
seenRequests += 1;
|
|
49
|
+
if (seenRequests % 250 === 0) {
|
|
50
|
+
pruneExpiredBuckets(currentTime);
|
|
51
|
+
}
|
|
52
|
+
next();
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export function createConcurrentConnectionLimiter(options) {
|
|
56
|
+
const activeConnections = new Map();
|
|
57
|
+
const keyGenerator = options.keyGenerator ?? getClientIp;
|
|
58
|
+
return function concurrentConnectionLimiter(req, res, next) {
|
|
59
|
+
const key = keyGenerator(req);
|
|
60
|
+
const current = activeConnections.get(key) ?? 0;
|
|
61
|
+
if (current >= options.maxConnections) {
|
|
62
|
+
writeRateLimitResponse(req, res, 60, "Too many concurrent stream connections. Retry after {retryAfterSeconds} seconds.", options.onLimit);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
activeConnections.set(key, current + 1);
|
|
66
|
+
let released = false;
|
|
67
|
+
const release = () => {
|
|
68
|
+
if (released) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
released = true;
|
|
72
|
+
const remaining = activeConnections.get(key) ?? 0;
|
|
73
|
+
if (remaining <= 1) {
|
|
74
|
+
activeConnections.delete(key);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
activeConnections.set(key, remaining - 1);
|
|
78
|
+
};
|
|
79
|
+
res.on("finish", release);
|
|
80
|
+
res.on("close", release);
|
|
81
|
+
req.on("close", release);
|
|
82
|
+
next();
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
export function resolveApiToken({ envToken, tokenPath, exists = existsSync, readFile = readFileSync, }) {
|
|
3
|
+
const trimmedEnvToken = envToken?.trim();
|
|
4
|
+
if (trimmedEnvToken) {
|
|
5
|
+
return trimmedEnvToken;
|
|
6
|
+
}
|
|
7
|
+
if (exists(tokenPath)) {
|
|
8
|
+
const persistedToken = readFile(tokenPath, "utf-8").trim();
|
|
9
|
+
return persistedToken.length > 0 ? persistedToken : null;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export function assertAuthenticationConfigured(options) {
|
|
14
|
+
void options;
|
|
15
|
+
}
|
|
16
|
+
export function createHealthPayload(now = new Date()) {
|
|
17
|
+
return {
|
|
18
|
+
status: "ok",
|
|
19
|
+
timestamp: now.toISOString(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function createPublicConfigPayload(config) {
|
|
23
|
+
if (!config.entraAuthEnabled) {
|
|
24
|
+
return {
|
|
25
|
+
appName: "Chapterhouse",
|
|
26
|
+
entraAuthEnabled: false,
|
|
27
|
+
standalone: config.standaloneMode,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
appName: "Chapterhouse",
|
|
32
|
+
entraAuthEnabled: true,
|
|
33
|
+
entraClientId: config.entraClientId,
|
|
34
|
+
entraTenantId: config.entraTenantId,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function shouldServeSpaPath(pathname) {
|
|
38
|
+
return !pathname.startsWith("/api/")
|
|
39
|
+
&& pathname !== "/stream"
|
|
40
|
+
&& pathname !== "/status"
|
|
41
|
+
&& pathname !== "/health";
|
|
42
|
+
}
|
|
43
|
+
export function getDisplayHost(host) {
|
|
44
|
+
return host === "0.0.0.0" || host === "::" || host === "127.0.0.1" || host === "::1" ? "localhost" : host;
|
|
45
|
+
}
|
|
46
|
+
export function buildHistoryEntries(pageIds, options) {
|
|
47
|
+
const resolveWikiPath = options?.resolveWikiPath ?? ((pageId) => pageId);
|
|
48
|
+
const stat = options?.stat;
|
|
49
|
+
return pageIds
|
|
50
|
+
.map((pageId) => {
|
|
51
|
+
try {
|
|
52
|
+
const fullPath = resolveWikiPath(pageId);
|
|
53
|
+
const mtime = stat ? stat(fullPath).mtimeMs : 0;
|
|
54
|
+
return { path: pageId, mtime };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return { path: pageId, mtime: 0 };
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=server-runtime.js.map
|