@neuralnomads/codenomad-dev 0.18.0-dev-20260715-07e8ce68 → 0.18.0-dev-20260716-40cee071
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 +12 -0
- package/THIRD_PARTY_NOTICES.md +26 -0
- package/dist/events/bus.js +13 -0
- package/dist/events/bus.test.js +37 -0
- package/dist/index.js +33 -56
- package/dist/index.test.js +32 -0
- package/dist/server/__tests__/remote-proxy.test.js +102 -6
- package/dist/server/http-server.js +2 -0
- package/dist/server/remote-proxy.js +96 -9
- package/dist/server/routes/usage.js +18 -0
- package/dist/server/routes/usage.test.js +26 -0
- package/dist/server/routes/workspaces.js +32 -1
- package/dist/server/routes/workspaces.test.js +75 -0
- package/dist/settings/binaries.js +4 -1
- package/dist/settings/binaries.test.js +24 -0
- package/dist/shutdown.js +75 -0
- package/dist/shutdown.test.js +111 -0
- package/dist/usage/providers/api-key.js +171 -0
- package/dist/usage/providers/google.js +171 -0
- package/dist/usage/providers/minimax.js +109 -0
- package/dist/usage/providers/oauth.js +111 -0
- package/dist/usage/providers/special.js +148 -0
- package/dist/usage/service.js +83 -0
- package/dist/usage/service.test.js +59 -0
- package/dist/usage/shared.js +130 -0
- package/dist/usage/shared.test.js +33 -0
- package/dist/usage/types.js +1 -0
- package/dist/workspaces/__tests__/spawn.test.js +64 -7
- package/dist/workspaces/__tests__/workspace-identity.test.js +177 -25
- package/dist/workspaces/launch-cleanup.js +132 -0
- package/dist/workspaces/launch-cleanup.test.js +33 -0
- package/dist/workspaces/manager.js +393 -135
- package/dist/workspaces/manager.test.js +318 -0
- package/dist/workspaces/process-identity.js +408 -0
- package/dist/workspaces/process-identity.test.js +165 -0
- package/dist/workspaces/runtime.js +567 -122
- package/dist/workspaces/runtime.test.js +603 -0
- package/dist/workspaces/spawn.js +86 -15
- package/package.json +1 -1
- package/public/assets/{FilesTab-EPvldvn1.js → FilesTab-BjztpSdj.js} +2 -2
- package/public/assets/{GitChangesTab-ofjovmv2.js → GitChangesTab-B9XraSoR.js} +2 -2
- package/public/assets/{SplitFilePanel-CyoPkcLd.js → SplitFilePanel-Cu_5-Z-E.js} +1 -1
- package/public/assets/StatusTab-D_IE79r-.js +1 -0
- package/public/assets/{align-justify-Bcf1-ZOl.js → align-justify-BKS9gyRk.js} +1 -1
- package/public/assets/{bundle-full-D5dzvToA.js → bundle-full-CXGXXlzR.js} +1 -1
- package/public/assets/{diff-viewer-DViXCI-4.js → diff-viewer-CSNOYoEB.js} +1 -1
- package/public/assets/{git-diff-vendor-CSgooKT_.js → git-diff-vendor-CYS3ApzA.js} +1 -1
- package/public/assets/index-B3YjNCh4.js +1 -0
- package/public/assets/index-B3ZD2_SU.js +1 -0
- package/public/assets/index-Bh2ofaQo.js +1 -0
- package/public/assets/index-CM0c9xRZ.js +1 -0
- package/public/assets/index-CdDxouJl.js +2 -0
- package/public/assets/index-D06Y2cFq.js +1 -0
- package/public/assets/{index-lf4nUOU1.js → index-DVc4ls8c.js} +1 -1
- package/public/assets/index-PmE51vPh.js +1 -0
- package/public/assets/{index-BMvFMsuy.css → index-Qj63IiSj.css} +1 -1
- package/public/assets/index-VY_zwmsG.js +1 -0
- package/public/assets/index-qiiH7egV.js +1 -0
- package/public/assets/{index-UbL8tz_V.js → index-sLe9hEfQ.js} +1 -1
- package/public/assets/{loading-CZ_tt3h0.js → loading-CDmt9OJU.js} +1 -1
- package/public/assets/main-CTN6Wwz1.js +68 -0
- package/public/assets/{markdown-G11aN6hz.js → markdown-1ZLSxCFT.js} +55 -44
- package/public/assets/monaco-viewer-D6JgH76W.js +26 -0
- package/public/assets/{tool-call-BsSXWErm.js → tool-call-CyXb63LH.js} +32 -32
- package/public/assets/{unified-picker-BRNI3UvE.js → unified-picker-DCCdSVwI.js} +1 -1
- package/public/assets/{wrap-text-B7up-uv7.js → wrap-text-pNMdTIq3.js} +1 -1
- package/public/index.html +5 -5
- package/public/loading.html +5 -5
- package/public/sw.js +1 -1
- package/public/assets/StatusTab-Bgu1Fh24.js +0 -1
- package/public/assets/index-3KsjbWGP.js +0 -1
- package/public/assets/index-B0WK8-cF.js +0 -1
- package/public/assets/index-BDv_iVtj.js +0 -1
- package/public/assets/index-BjxZvyb9.js +0 -2
- package/public/assets/index-BleKf9Nj.js +0 -1
- package/public/assets/index-CiIQ43NW.js +0 -1
- package/public/assets/index-DBquqL6Q.js +0 -1
- package/public/assets/index-yf9zolv4.js +0 -1
- package/public/assets/index-ypaL7i_s.js +0 -1
- package/public/assets/main-CAkg_fp_.js +0 -68
- package/public/assets/monaco-viewer-CZefBfct.js +0 -26
|
@@ -7,6 +7,8 @@ import { resolveWorktreeDirectory } from "../../workspaces/worktree-directory";
|
|
|
7
7
|
const WorkspaceCreateSchema = z.object({
|
|
8
8
|
path: z.string(),
|
|
9
9
|
name: z.string().optional(),
|
|
10
|
+
binaryPath: z.string().trim().min(1).max(4096).optional(),
|
|
11
|
+
requestId: z.string().trim().min(1).max(128).optional(),
|
|
10
12
|
forceNew: z.boolean().optional(),
|
|
11
13
|
});
|
|
12
14
|
const WorkspaceCloneSchema = z.object({
|
|
@@ -14,6 +16,9 @@ const WorkspaceCloneSchema = z.object({
|
|
|
14
16
|
destinationPath: z.string().trim().min(1, "Destination path is required"),
|
|
15
17
|
cleanup: z.boolean().optional(),
|
|
16
18
|
});
|
|
19
|
+
const WorkspaceCreationReleaseSchema = z.object({
|
|
20
|
+
requestId: z.string().trim().min(1).max(128),
|
|
21
|
+
});
|
|
17
22
|
const WorkspaceFilesQuerySchema = z.object({
|
|
18
23
|
path: z.string().optional(),
|
|
19
24
|
});
|
|
@@ -52,7 +57,11 @@ export function registerWorkspaceRoutes(app, deps) {
|
|
|
52
57
|
app.post("/api/workspaces", async (request, reply) => {
|
|
53
58
|
try {
|
|
54
59
|
const body = WorkspaceCreateSchema.parse(request.body ?? {});
|
|
55
|
-
const result = await deps.workspaceManager.create(body.path, body.name, {
|
|
60
|
+
const result = await deps.workspaceManager.create(body.path, body.name, {
|
|
61
|
+
binaryPath: body.binaryPath,
|
|
62
|
+
requestId: body.requestId,
|
|
63
|
+
forceNew: body.forceNew,
|
|
64
|
+
});
|
|
56
65
|
reply.code(201);
|
|
57
66
|
return result.created ? result.workspace : { ...result.workspace, reused: true };
|
|
58
67
|
}
|
|
@@ -85,6 +94,28 @@ export function registerWorkspaceRoutes(app, deps) {
|
|
|
85
94
|
await deps.workspaceManager.delete(request.params.id);
|
|
86
95
|
reply.code(204);
|
|
87
96
|
});
|
|
97
|
+
app.post("/api/workspaces/creation/cancel", async (request, reply) => {
|
|
98
|
+
const parsed = WorkspaceCreationReleaseSchema.safeParse(request.body ?? {});
|
|
99
|
+
if (!parsed.success) {
|
|
100
|
+
reply.code(400).type("text/plain").send("Invalid workspace creation request");
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await deps.workspaceManager.cancelCreationRequest(parsed.data.requestId);
|
|
104
|
+
reply.code(204);
|
|
105
|
+
});
|
|
106
|
+
app.post("/api/workspaces/:id/creation/release", async (request, reply) => {
|
|
107
|
+
const parsed = WorkspaceCreationReleaseSchema.safeParse(request.body ?? {});
|
|
108
|
+
if (!parsed.success) {
|
|
109
|
+
reply.code(400).type("text/plain").send("Invalid workspace creation request");
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const body = parsed.data;
|
|
113
|
+
if (!deps.workspaceManager.releaseCreationRequest(request.params.id, body.requestId)) {
|
|
114
|
+
reply.code(404).type("text/plain").send("Workspace creation request not found");
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
reply.code(204);
|
|
118
|
+
});
|
|
88
119
|
app.get("/api/workspaces/:id/files", async (request, reply) => {
|
|
89
120
|
try {
|
|
90
121
|
const query = WorkspaceFilesQuerySchema.parse(request.query ?? {});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import Fastify from "fastify";
|
|
4
|
+
import { registerWorkspaceRoutes } from "./workspaces";
|
|
5
|
+
describe("workspace routes", () => {
|
|
6
|
+
it("forwards a validated explicit binary path when creating a workspace", async () => {
|
|
7
|
+
const calls = [];
|
|
8
|
+
const app = Fastify({ logger: false });
|
|
9
|
+
const descriptor = {
|
|
10
|
+
id: "workspace",
|
|
11
|
+
path: "C:/work",
|
|
12
|
+
status: "ready",
|
|
13
|
+
proxyPath: "/workspaces/workspace/instance",
|
|
14
|
+
binaryId: "C:/tools/opencode.exe",
|
|
15
|
+
binaryLabel: "opencode.exe",
|
|
16
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
17
|
+
updatedAt: "2026-01-01T00:00:00.000Z",
|
|
18
|
+
};
|
|
19
|
+
const workspaceManager = {
|
|
20
|
+
create: async (...args) => {
|
|
21
|
+
calls.push(args);
|
|
22
|
+
return { workspace: descriptor, created: true };
|
|
23
|
+
},
|
|
24
|
+
releaseCreationRequest: (workspaceId, requestId) => workspaceId === descriptor.id && requestId === "restore-request",
|
|
25
|
+
cancelCreationRequest: async (requestId) => {
|
|
26
|
+
calls.push(["cancel", requestId]);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
registerWorkspaceRoutes(app, { workspaceManager });
|
|
30
|
+
const response = await app.inject({
|
|
31
|
+
method: "POST",
|
|
32
|
+
url: "/api/workspaces",
|
|
33
|
+
payload: {
|
|
34
|
+
path: "C:/work",
|
|
35
|
+
name: "Work",
|
|
36
|
+
binaryPath: " C:/tools/opencode.exe ",
|
|
37
|
+
requestId: " restore-request ",
|
|
38
|
+
forceNew: true,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
assert.equal(response.statusCode, 201);
|
|
42
|
+
assert.deepEqual(calls, [["C:/work", "Work", {
|
|
43
|
+
binaryPath: "C:/tools/opencode.exe",
|
|
44
|
+
requestId: "restore-request",
|
|
45
|
+
forceNew: true,
|
|
46
|
+
}]]);
|
|
47
|
+
const released = await app.inject({
|
|
48
|
+
method: "POST",
|
|
49
|
+
url: "/api/workspaces/workspace/creation/release",
|
|
50
|
+
payload: { requestId: "restore-request" },
|
|
51
|
+
});
|
|
52
|
+
assert.equal(released.statusCode, 204);
|
|
53
|
+
const wrongRelease = await app.inject({
|
|
54
|
+
method: "POST",
|
|
55
|
+
url: "/api/workspaces/workspace/creation/release",
|
|
56
|
+
payload: { requestId: "other-request" },
|
|
57
|
+
});
|
|
58
|
+
assert.equal(wrongRelease.statusCode, 404);
|
|
59
|
+
const cancelled = await app.inject({
|
|
60
|
+
method: "POST",
|
|
61
|
+
url: "/api/workspaces/creation/cancel",
|
|
62
|
+
payload: { requestId: "restore-request" },
|
|
63
|
+
});
|
|
64
|
+
assert.equal(cancelled.statusCode, 204);
|
|
65
|
+
assert.deepEqual(calls.at(-1), ["cancel", "restore-request"]);
|
|
66
|
+
const invalid = await app.inject({
|
|
67
|
+
method: "POST",
|
|
68
|
+
url: "/api/workspaces",
|
|
69
|
+
payload: { path: "C:/work", binaryPath: "x".repeat(4097) },
|
|
70
|
+
});
|
|
71
|
+
assert.equal(invalid.statusCode, 400);
|
|
72
|
+
assert.equal(calls.length, 2);
|
|
73
|
+
await app.close();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -23,10 +23,13 @@ export class BinaryResolver {
|
|
|
23
23
|
return readUiBinaries(this.settings);
|
|
24
24
|
}
|
|
25
25
|
resolveDefault() {
|
|
26
|
+
return this.resolve();
|
|
27
|
+
}
|
|
28
|
+
resolve(explicitPath) {
|
|
26
29
|
const binaries = this.list();
|
|
27
30
|
const configuredDefault = readDefaultBinaryPath(this.settings);
|
|
28
31
|
const fallback = binaries[0]?.path;
|
|
29
|
-
const path = configuredDefault
|
|
32
|
+
const path = explicitPath?.trim() || configuredDefault || fallback || "opencode";
|
|
30
33
|
const entry = binaries.find((b) => b.path === path);
|
|
31
34
|
return {
|
|
32
35
|
path,
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { BinaryResolver } from "./binaries";
|
|
4
|
+
describe("BinaryResolver", () => {
|
|
5
|
+
it("uses an explicit workspace binary without changing the configured default", () => {
|
|
6
|
+
const settings = {
|
|
7
|
+
getOwner(scope, owner) {
|
|
8
|
+
if (scope === "config" && owner === "server")
|
|
9
|
+
return { opencodeBinary: "default-opencode" };
|
|
10
|
+
if (scope === "state" && owner === "ui") {
|
|
11
|
+
return { opencodeBinaries: [{ path: "saved-opencode", label: "Saved", version: "1.2.3" }] };
|
|
12
|
+
}
|
|
13
|
+
return {};
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
const resolver = new BinaryResolver(settings);
|
|
17
|
+
assert.deepEqual(resolver.resolve("saved-opencode"), {
|
|
18
|
+
path: "saved-opencode",
|
|
19
|
+
label: "Saved",
|
|
20
|
+
version: "1.2.3",
|
|
21
|
+
});
|
|
22
|
+
assert.equal(resolver.resolveDefault().path, "default-opencode");
|
|
23
|
+
});
|
|
24
|
+
});
|
package/dist/shutdown.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export class ServerShutdownError extends Error {
|
|
2
|
+
constructor(failures) {
|
|
3
|
+
super(`Server shutdown failed while stopping: ${failures.map((failure) => failure.resource).join(", ")}`);
|
|
4
|
+
this.name = "ServerShutdownError";
|
|
5
|
+
this.failures = failures;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function createServerShutdownHandler(options) {
|
|
9
|
+
const forceExit = options.forceExit ?? ((code) => process.exit(code));
|
|
10
|
+
const setExitCode = options.setExitCode ?? ((code) => {
|
|
11
|
+
process.exitCode = code;
|
|
12
|
+
});
|
|
13
|
+
let shutdownPromise;
|
|
14
|
+
return (signal) => {
|
|
15
|
+
if (shutdownPromise) {
|
|
16
|
+
options.logger.error({ signal }, "Additional shutdown signal received; forcing nonzero exit");
|
|
17
|
+
forceExit(1);
|
|
18
|
+
return shutdownPromise;
|
|
19
|
+
}
|
|
20
|
+
options.logger.info({ signal }, "Received shutdown signal, stopping workspaces and server");
|
|
21
|
+
shutdownPromise = (async () => {
|
|
22
|
+
try {
|
|
23
|
+
await options.shutdown();
|
|
24
|
+
options.logger.info({}, "Shutdown complete");
|
|
25
|
+
setExitCode(0);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
options.logger.error({ err: error }, "Server shutdown incomplete; forcing nonzero exit");
|
|
29
|
+
forceExit(1);
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
return shutdownPromise;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function orchestrateServerShutdown(operations, logger, workspaceAttempts = 2) {
|
|
36
|
+
const failures = [];
|
|
37
|
+
const run = async (resource, operation) => {
|
|
38
|
+
try {
|
|
39
|
+
await operation();
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
failures.push({ resource, error });
|
|
43
|
+
logger.error({ err: error, resource }, `${resource} shutdown failed`);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
await run("Instance event bridge", operations.stopInstanceEventBridge);
|
|
47
|
+
await run("SideCar manager", operations.stopSidecars);
|
|
48
|
+
await run("Client connection manager", operations.stopClientConnections);
|
|
49
|
+
await run("Remote proxy session manager", operations.stopRemoteProxySessions);
|
|
50
|
+
const attempts = Math.max(1, Math.floor(workspaceAttempts));
|
|
51
|
+
let workspaceFailure;
|
|
52
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
53
|
+
try {
|
|
54
|
+
await operations.stopWorkspaces();
|
|
55
|
+
workspaceFailure = undefined;
|
|
56
|
+
logger.info({ attempt }, "Workspace manager shutdown complete");
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
workspaceFailure = error;
|
|
61
|
+
if (attempt < attempts) {
|
|
62
|
+
logger.warn({ err: error, attempt, attempts }, "Workspace manager shutdown failed; retrying cleanup");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (workspaceFailure !== undefined) {
|
|
67
|
+
failures.push({ resource: "Workspace manager", error: workspaceFailure });
|
|
68
|
+
logger.error({ err: workspaceFailure, attempts }, "Workspace manager shutdown failed");
|
|
69
|
+
}
|
|
70
|
+
await run("HTTP servers", operations.stopHttpServers);
|
|
71
|
+
await run("Release monitor", operations.stopReleaseMonitor);
|
|
72
|
+
if (failures.length > 0) {
|
|
73
|
+
throw new ServerShutdownError(failures);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { describe, it } from "node:test";
|
|
3
|
+
import { createServerShutdownHandler, orchestrateServerShutdown, ServerShutdownError, } from "./shutdown";
|
|
4
|
+
const logger = {
|
|
5
|
+
info: () => undefined,
|
|
6
|
+
warn: () => undefined,
|
|
7
|
+
error: () => undefined,
|
|
8
|
+
};
|
|
9
|
+
function operations(overrides = {}) {
|
|
10
|
+
return {
|
|
11
|
+
stopInstanceEventBridge: () => undefined,
|
|
12
|
+
stopSidecars: () => undefined,
|
|
13
|
+
stopClientConnections: () => undefined,
|
|
14
|
+
stopRemoteProxySessions: () => undefined,
|
|
15
|
+
stopWorkspaces: () => undefined,
|
|
16
|
+
stopHttpServers: () => undefined,
|
|
17
|
+
stopReleaseMonitor: () => undefined,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
describe("server shutdown orchestration", () => {
|
|
22
|
+
it("retries workspace cleanup before reporting success", async () => {
|
|
23
|
+
let attempts = 0;
|
|
24
|
+
await orchestrateServerShutdown(operations({
|
|
25
|
+
stopWorkspaces: () => {
|
|
26
|
+
attempts += 1;
|
|
27
|
+
if (attempts === 1)
|
|
28
|
+
throw new Error("detached process still alive");
|
|
29
|
+
},
|
|
30
|
+
}), logger);
|
|
31
|
+
assert.equal(attempts, 2);
|
|
32
|
+
});
|
|
33
|
+
it("stops remote proxy sessions before workspace and HTTP shutdown", async () => {
|
|
34
|
+
const closed = [];
|
|
35
|
+
await orchestrateServerShutdown(operations({
|
|
36
|
+
stopRemoteProxySessions: () => { closed.push("remote-proxy"); },
|
|
37
|
+
stopWorkspaces: () => { closed.push("workspaces"); },
|
|
38
|
+
stopHttpServers: () => { closed.push("http"); },
|
|
39
|
+
}), logger);
|
|
40
|
+
assert.deepEqual(closed, ["remote-proxy", "workspaces", "http"]);
|
|
41
|
+
});
|
|
42
|
+
it("closes other resources and rejects with the concrete workspace failure", async () => {
|
|
43
|
+
const closed = [];
|
|
44
|
+
const cleanupFailure = new Error("workspace abc POSIX process group is still alive");
|
|
45
|
+
let attempts = 0;
|
|
46
|
+
await assert.rejects(orchestrateServerShutdown(operations({
|
|
47
|
+
stopWorkspaces: () => {
|
|
48
|
+
attempts += 1;
|
|
49
|
+
throw cleanupFailure;
|
|
50
|
+
},
|
|
51
|
+
stopHttpServers: () => {
|
|
52
|
+
closed.push("http");
|
|
53
|
+
},
|
|
54
|
+
stopReleaseMonitor: () => {
|
|
55
|
+
closed.push("release-monitor");
|
|
56
|
+
},
|
|
57
|
+
}), logger), (error) => {
|
|
58
|
+
assert.ok(error instanceof ServerShutdownError);
|
|
59
|
+
assert.equal(error.failures[0]?.resource, "Workspace manager");
|
|
60
|
+
assert.strictEqual(error.failures[0]?.error, cleanupFailure);
|
|
61
|
+
return true;
|
|
62
|
+
});
|
|
63
|
+
assert.equal(attempts, 2);
|
|
64
|
+
assert.deepEqual(closed, ["http", "release-monitor"]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
describe("server shutdown signal boundary", () => {
|
|
68
|
+
it("forces a nonzero exit after failed bounded cleanup returns", async () => {
|
|
69
|
+
const calls = [];
|
|
70
|
+
const exits = [];
|
|
71
|
+
const handler = createServerShutdownHandler({
|
|
72
|
+
shutdown: async () => {
|
|
73
|
+
calls.push("cleanup");
|
|
74
|
+
throw new Error("retained child survived");
|
|
75
|
+
},
|
|
76
|
+
logger: {
|
|
77
|
+
info: () => calls.push("info"),
|
|
78
|
+
warn: () => undefined,
|
|
79
|
+
error: () => calls.push("error"),
|
|
80
|
+
},
|
|
81
|
+
forceExit: (code) => {
|
|
82
|
+
calls.push("force-exit");
|
|
83
|
+
exits.push(code);
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
await handler("SIGTERM");
|
|
87
|
+
assert.deepEqual(exits, [1]);
|
|
88
|
+
assert.deepEqual(calls, ["info", "cleanup", "error", "force-exit"]);
|
|
89
|
+
});
|
|
90
|
+
it("escalates a second signal while shutdown is still pending", async () => {
|
|
91
|
+
let finishCleanup;
|
|
92
|
+
const cleanup = new Promise((resolve) => {
|
|
93
|
+
finishCleanup = resolve;
|
|
94
|
+
});
|
|
95
|
+
const exits = [];
|
|
96
|
+
const exitCodes = [];
|
|
97
|
+
const handler = createServerShutdownHandler({
|
|
98
|
+
shutdown: () => cleanup,
|
|
99
|
+
logger,
|
|
100
|
+
forceExit: (code) => exits.push(code),
|
|
101
|
+
setExitCode: (code) => exitCodes.push(code),
|
|
102
|
+
});
|
|
103
|
+
const first = handler("SIGINT");
|
|
104
|
+
const second = handler("SIGTERM");
|
|
105
|
+
assert.strictEqual(first, second);
|
|
106
|
+
assert.deepEqual(exits, [1]);
|
|
107
|
+
finishCleanup();
|
|
108
|
+
await first;
|
|
109
|
+
assert.deepEqual(exitCodes, [0]);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { fetchJson, getCredential, notConfigured, resolveWindowLabel, safeFetch, toNumber, toTimestamp, toUsageWindow, } from "../shared";
|
|
2
|
+
const kimiAliases = ["kimi-for-coding", "kimi"];
|
|
3
|
+
const kimi = {
|
|
4
|
+
id: "kimi-for-coding",
|
|
5
|
+
name: "Kimi for Coding",
|
|
6
|
+
aliases: kimiAliases,
|
|
7
|
+
async fetchQuota() {
|
|
8
|
+
const key = getCredential(kimiAliases, ["key", "token"]);
|
|
9
|
+
if (!key)
|
|
10
|
+
return notConfigured(this.id, this.name);
|
|
11
|
+
return safeFetch(this.id, this.name, async () => {
|
|
12
|
+
const payload = await fetchJson("https://api.kimi.com/coding/v1/usages", {
|
|
13
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
14
|
+
});
|
|
15
|
+
const windows = {};
|
|
16
|
+
const usage = payload?.usage;
|
|
17
|
+
if (usage) {
|
|
18
|
+
const limit = toNumber(usage.limit);
|
|
19
|
+
const remaining = toNumber(usage.remaining);
|
|
20
|
+
windows.weekly = toUsageWindow({
|
|
21
|
+
usedPercent: limit && remaining !== null ? 100 - (remaining / limit) * 100 : null,
|
|
22
|
+
resetAt: toTimestamp(usage.resetTime),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
for (const item of Array.isArray(payload?.limits) ? payload.limits : []) {
|
|
26
|
+
const duration = toNumber(item?.window?.duration);
|
|
27
|
+
const unit = item?.window?.timeUnit;
|
|
28
|
+
const multiplier = unit === "TIME_UNIT_MINUTE" ? 60 : unit === "TIME_UNIT_HOUR" ? 3600 : unit === "TIME_UNIT_DAY" ? 86400 : null;
|
|
29
|
+
const seconds = duration !== null && multiplier ? duration * multiplier : null;
|
|
30
|
+
const limit = toNumber(item?.detail?.limit);
|
|
31
|
+
const remaining = toNumber(item?.detail?.remaining);
|
|
32
|
+
windows[resolveWindowLabel(seconds)] = toUsageWindow({
|
|
33
|
+
usedPercent: limit && remaining !== null ? 100 - (remaining / limit) * 100 : null,
|
|
34
|
+
windowSeconds: seconds,
|
|
35
|
+
resetAt: toTimestamp(item?.detail?.resetTime),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
return { windows };
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const nanoAliases = ["nano-gpt", "nanogpt", "nano_gpt"];
|
|
43
|
+
const nanoGpt = {
|
|
44
|
+
id: "nano-gpt",
|
|
45
|
+
name: "NanoGPT",
|
|
46
|
+
aliases: nanoAliases,
|
|
47
|
+
async fetchQuota() {
|
|
48
|
+
const key = getCredential(nanoAliases, ["key", "token"]);
|
|
49
|
+
if (!key)
|
|
50
|
+
return notConfigured(this.id, this.name);
|
|
51
|
+
return safeFetch(this.id, this.name, async () => {
|
|
52
|
+
const payload = await fetchJson("https://nano-gpt.com/api/subscription/v1/usage", {
|
|
53
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
54
|
+
});
|
|
55
|
+
const windows = {};
|
|
56
|
+
for (const [label, source] of [["daily", payload?.daily], ["monthly", payload?.monthly]]) {
|
|
57
|
+
if (!source)
|
|
58
|
+
continue;
|
|
59
|
+
const fraction = toNumber(source.percentUsed);
|
|
60
|
+
const used = toNumber(source.used);
|
|
61
|
+
const limit = toNumber(source.limit ?? source.limits?.[label]);
|
|
62
|
+
windows[label] = toUsageWindow({
|
|
63
|
+
usedPercent: fraction !== null ? fraction * 100 : used !== null && limit ? (used / limit) * 100 : null,
|
|
64
|
+
windowSeconds: label === "daily" ? 86400 : null,
|
|
65
|
+
resetAt: toTimestamp(source.resetAt ?? payload?.period?.currentPeriodEnd),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return { windows };
|
|
69
|
+
});
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
const openRouterAliases = ["openrouter"];
|
|
73
|
+
const openRouter = {
|
|
74
|
+
id: "openrouter",
|
|
75
|
+
name: "OpenRouter",
|
|
76
|
+
aliases: openRouterAliases,
|
|
77
|
+
async fetchQuota() {
|
|
78
|
+
const key = getCredential(openRouterAliases, ["key", "token"]);
|
|
79
|
+
if (!key)
|
|
80
|
+
return notConfigured(this.id, this.name);
|
|
81
|
+
return safeFetch(this.id, this.name, async () => {
|
|
82
|
+
const payload = await fetchJson("https://openrouter.ai/api/v1/credits", {
|
|
83
|
+
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" },
|
|
84
|
+
});
|
|
85
|
+
const total = toNumber(payload?.data?.total_credits);
|
|
86
|
+
const used = toNumber(payload?.data?.total_usage);
|
|
87
|
+
const remaining = total !== null && used !== null ? Math.max(0, total - used) : null;
|
|
88
|
+
return {
|
|
89
|
+
windows: {
|
|
90
|
+
credits: toUsageWindow({
|
|
91
|
+
usedPercent: total && used !== null ? (used / total) * 100 : null,
|
|
92
|
+
valueLabel: remaining !== null && total !== null ? `$${remaining.toFixed(2)} / $${total.toFixed(2)}` : null,
|
|
93
|
+
}),
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
function createTokenLimitProvider(input) {
|
|
100
|
+
return {
|
|
101
|
+
id: input.id,
|
|
102
|
+
name: input.name,
|
|
103
|
+
aliases: input.aliases,
|
|
104
|
+
async fetchQuota() {
|
|
105
|
+
const key = getCredential(input.aliases, ["key", "token"]);
|
|
106
|
+
if (!key)
|
|
107
|
+
return notConfigured(this.id, this.name);
|
|
108
|
+
return safeFetch(this.id, this.name, async () => {
|
|
109
|
+
const payload = await fetchJson(input.url, { headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json" } });
|
|
110
|
+
const windows = {};
|
|
111
|
+
for (const limit of Array.isArray(payload?.data?.limits) ? payload.data.limits : []) {
|
|
112
|
+
if (limit?.type !== "TOKENS_LIMIT" && limit?.type !== "TIME_LIMIT")
|
|
113
|
+
continue;
|
|
114
|
+
const duration = toNumber(limit.number);
|
|
115
|
+
const seconds = limit.type === "TIME_LIMIT" ? 30 * 86400 : limit.unit === 3 && duration ? duration * 3600 : null;
|
|
116
|
+
const label = limit.type === "TIME_LIMIT" ? "mcp-tools" : resolveWindowLabel(seconds);
|
|
117
|
+
windows[label] = toUsageWindow({
|
|
118
|
+
usedPercent: toNumber(limit.percentage),
|
|
119
|
+
windowSeconds: seconds,
|
|
120
|
+
resetAt: toTimestamp(limit.nextResetTime),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
return { windows };
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const zai = createTokenLimitProvider({
|
|
129
|
+
id: "zai-coding-plan",
|
|
130
|
+
name: "z.ai",
|
|
131
|
+
aliases: ["zai-coding-plan", "zai", "z.ai"],
|
|
132
|
+
url: "https://api.z.ai/api/monitor/usage/quota/limit",
|
|
133
|
+
});
|
|
134
|
+
const zhipu = createTokenLimitProvider({
|
|
135
|
+
id: "zhipuai-coding-plan",
|
|
136
|
+
name: "Zhipu AI Coding Plan",
|
|
137
|
+
aliases: ["zhipuai-coding-plan", "zhipuai", "zhipu"],
|
|
138
|
+
url: "https://open.bigmodel.cn/api/monitor/usage/quota/limit",
|
|
139
|
+
});
|
|
140
|
+
const waferAliases = ["wafer", "wafer-ai", "wafer_ai", "wafer.ai"];
|
|
141
|
+
const wafer = {
|
|
142
|
+
id: "wafer",
|
|
143
|
+
name: "Wafer.ai",
|
|
144
|
+
aliases: waferAliases,
|
|
145
|
+
async fetchQuota() {
|
|
146
|
+
const key = getCredential(waferAliases, ["key", "token"]);
|
|
147
|
+
if (!key)
|
|
148
|
+
return notConfigured(this.id, this.name);
|
|
149
|
+
return safeFetch(this.id, this.name, async () => {
|
|
150
|
+
const payload = await fetchJson("https://pass.wafer.ai/v1/inference/quota", {
|
|
151
|
+
headers: { Authorization: `Bearer ${key}`, "Accept-Encoding": "identity" },
|
|
152
|
+
});
|
|
153
|
+
const remaining = toNumber(payload?.remaining_included_requests);
|
|
154
|
+
const limit = toNumber(payload?.included_request_limit);
|
|
155
|
+
const startAt = toTimestamp(payload?.window_start);
|
|
156
|
+
const resetAt = toTimestamp(payload?.window_end);
|
|
157
|
+
const seconds = startAt !== null && resetAt !== null ? Math.round((resetAt - startAt) / 1000) : 5 * 3600;
|
|
158
|
+
return {
|
|
159
|
+
windows: {
|
|
160
|
+
[resolveWindowLabel(seconds)]: toUsageWindow({
|
|
161
|
+
usedPercent: toNumber(payload?.current_period_used_percent),
|
|
162
|
+
windowSeconds: seconds,
|
|
163
|
+
resetAt,
|
|
164
|
+
valueLabel: remaining !== null && limit !== null ? `${remaining} / ${limit}` : null,
|
|
165
|
+
}),
|
|
166
|
+
},
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
export const apiKeyProviders = [kimi, nanoGpt, openRouter, zai, zhipu, wafer];
|