@opencode/server 0.0.0-dev-19794 → 0.0.0-dev-19808
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/dist/api.js +10 -9
- package/dist/auth.js +12 -17
- package/dist/chunks/workerd-00nn3k4b.js +20 -0
- package/dist/chunks/workerd-04xw3v55.js +23 -0
- package/dist/chunks/workerd-19c9ysa1.js +61 -0
- package/dist/chunks/workerd-1c4qas2w.js +346 -0
- package/dist/chunks/workerd-254zvkj6.js +22 -0
- package/dist/chunks/workerd-2rvsests.js +99 -0
- package/dist/chunks/workerd-2sy3c7de.js +83 -0
- package/dist/chunks/workerd-3kzx88w9.js +26 -0
- package/dist/chunks/workerd-4g0fa6m0.js +32 -0
- package/dist/chunks/workerd-4vn5bvkr.js +25 -0
- package/dist/chunks/workerd-5rwqcaw2.js +33 -0
- package/dist/chunks/workerd-5w24ch67.js +23 -0
- package/dist/chunks/workerd-6gkz39ey.js +55 -0
- package/dist/chunks/workerd-6nvan4e0.js +17 -0
- package/dist/chunks/workerd-6ptwb1q1.js +149 -0
- package/dist/chunks/workerd-75c938bc.js +23 -0
- package/dist/chunks/workerd-7cfyat0f.js +37 -0
- package/dist/chunks/workerd-7rzt3wm8.js +13 -0
- package/dist/chunks/workerd-8bajbwjh.js +40 -0
- package/dist/chunks/workerd-9rqn6x4v.js +16 -0
- package/dist/chunks/workerd-a6yp8301.js +156 -0
- package/dist/chunks/workerd-arj41x43.js +77 -0
- package/dist/chunks/workerd-b0b71rwa.js +39 -0
- package/dist/chunks/workerd-bjrwanme.js +22 -0
- package/dist/chunks/workerd-bkqymx4n.js +26 -0
- package/dist/chunks/workerd-br99j6q1.js +35 -0
- package/dist/chunks/workerd-ceh2h0m9.js +25 -0
- package/dist/chunks/workerd-hezj1jdf.js +7 -0
- package/dist/chunks/workerd-hjnxtxmk.js +34 -0
- package/dist/chunks/workerd-j2pfq4q6.js +42 -0
- package/dist/chunks/workerd-m6wvgbfq.js +60 -0
- package/dist/chunks/workerd-mzr9gftb.js +36 -0
- package/dist/chunks/workerd-n0er2m55.js +163 -0
- package/dist/chunks/workerd-pnqq1zxv.js +29 -0
- package/dist/chunks/workerd-q0ycym90.js +13 -0
- package/dist/chunks/workerd-q4bn6dy8.js +39 -0
- package/dist/chunks/workerd-qnwpcgp7.js +22 -0
- package/dist/chunks/workerd-qzap92t7.js +139 -0
- package/dist/chunks/workerd-r7e9r02j.js +13 -0
- package/dist/chunks/workerd-rcg4y18n.js +59 -0
- package/dist/chunks/workerd-req22b70.js +22 -0
- package/dist/chunks/workerd-smaf4a18.js +28 -0
- package/dist/chunks/workerd-t8yzmczr.js +18 -0
- package/dist/chunks/workerd-x1haxfjx.js +21 -0
- package/dist/chunks/workerd-yc7nft6q.js +5 -0
- package/dist/chunks/workerd-ysabas57.js +13 -0
- package/dist/chunks/workerd-z5shrcz9.js +19 -0
- package/dist/chunks/workerd-zp2jm6dt.js +15 -0
- package/dist/chunks/workerd-zyv89x5n.js +30 -0
- package/dist/cors.js +10 -36
- package/dist/event-feed.js +20 -60
- package/dist/fetch.js +53 -11
- package/dist/handlers/agent.js +11 -17
- package/dist/handlers/command.js +11 -5
- package/dist/handlers/config.js +11 -16
- package/dist/handlers/credential.js +11 -14
- package/dist/handlers/debug.js +11 -11
- package/dist/handlers/event.js +12 -27
- package/dist/handlers/form.js +11 -9
- package/dist/handlers/fs.js +11 -22
- package/dist/handlers/generate.js +11 -15
- package/dist/handlers/integration.js +10 -142
- package/dist/handlers/location.js +11 -16
- package/dist/handlers/mcp.js +11 -31
- package/dist/handlers/message.js +10 -42
- package/dist/handlers/migration.js +11 -7
- package/dist/handlers/model.js +11 -14
- package/dist/handlers/permission.js +11 -57
- package/dist/handlers/persistent-pty.js +12 -135
- package/dist/handlers/plugin.js +11 -47
- package/dist/handlers/project.js +11 -9
- package/dist/handlers/provider.js +11 -20
- package/dist/handlers/pty-socket.js +6 -4
- package/dist/handlers/pty.js +13 -107
- package/dist/handlers/reference.js +11 -5
- package/dist/handlers/rpc.js +11 -15
- package/dist/handlers/server.js +12 -13
- package/dist/handlers/session-error.js +12 -26
- package/dist/handlers/session.js +10 -311
- package/dist/handlers/shell.js +11 -25
- package/dist/handlers/skill.js +11 -5
- package/dist/handlers/vcs.js +11 -19
- package/dist/handlers/websearch.js +11 -31
- package/dist/handlers/worktree.js +11 -37
- package/dist/handlers.js +46 -33
- package/dist/location.js +15 -49
- package/dist/middleware/authorization.js +11 -49
- package/dist/middleware/form-location.js +10 -25
- package/dist/middleware/schema-error.js +8 -15
- package/dist/middleware/session-location.js +10 -19
- package/dist/options.js +5 -1
- package/dist/process.js +108 -28
- package/dist/pty-environment.js +12 -9
- package/dist/request-tracing.js +6 -2
- package/dist/routes.js +52 -118
- package/dist/server-info.js +12 -22
- package/dist/service-status.js +8 -11
- package/dist/workerd.js +80 -12
- package/package.json +8 -8
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Api2
|
|
3
|
+
} from "./workerd-z5shrcz9.js";
|
|
4
|
+
import {
|
|
5
|
+
CorsConfig2,
|
|
6
|
+
isAllowedRequestOrigin2
|
|
7
|
+
} from "./workerd-q4bn6dy8.js";
|
|
8
|
+
import {
|
|
9
|
+
runPtySocket2
|
|
10
|
+
} from "./workerd-hezj1jdf.js";
|
|
11
|
+
|
|
12
|
+
// src/handlers/persistent-pty.ts
|
|
13
|
+
import { PersistentPty } from "@opencode/core/persistent-pty";
|
|
14
|
+
import { PtyTicket } from "@opencode/core/pty/ticket";
|
|
15
|
+
import { ForbiddenError, PtyNotFoundError, ServiceUnavailableError } from "@opencode/protocol/errors";
|
|
16
|
+
import {
|
|
17
|
+
PTY_CONNECT_TICKET_QUERY,
|
|
18
|
+
PTY_CONNECT_TOKEN_HEADER,
|
|
19
|
+
PTY_CONNECT_TOKEN_HEADER_VALUE
|
|
20
|
+
} from "@opencode/protocol/groups/persistent-pty";
|
|
21
|
+
import { Effect, Queue, Semaphore } from "effect";
|
|
22
|
+
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
23
|
+
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi";
|
|
24
|
+
import { Socket } from "effect/unstable/socket";
|
|
25
|
+
var PersistentPtyHandler2 = HttpApiBuilder.group(Api2, "server.experimental", (handlers) => Effect.gen(function* () {
|
|
26
|
+
const tickets = yield* PtyTicket.Service;
|
|
27
|
+
const cors = yield* CorsConfig2;
|
|
28
|
+
const pty = yield* PersistentPty.Service;
|
|
29
|
+
return handlers.handle("persistentPty.read", Effect.fn(function* (ctx) {
|
|
30
|
+
return { data: yield* pty.read(ctx.params.sessionID, ctx.query.lines).pipe(mapUnavailable) };
|
|
31
|
+
})).handle("persistentPty.list", Effect.fn(function* (ctx) {
|
|
32
|
+
return { data: yield* pty.list(ctx.params.sessionID).pipe(mapUnavailable) };
|
|
33
|
+
})).handle("persistentPty.create", Effect.fn(function* (ctx) {
|
|
34
|
+
return {
|
|
35
|
+
data: yield* pty.create(ctx.params.sessionID, {
|
|
36
|
+
command: ctx.payload.command,
|
|
37
|
+
args: ctx.payload.args,
|
|
38
|
+
cwd: ctx.payload.cwd,
|
|
39
|
+
title: ctx.payload.title,
|
|
40
|
+
env: ctx.payload.env,
|
|
41
|
+
cols: ctx.payload.size?.cols,
|
|
42
|
+
rows: ctx.payload.size?.rows
|
|
43
|
+
}).pipe(Effect.catchTag("PersistentPty.UnavailableError", unavailable))
|
|
44
|
+
};
|
|
45
|
+
})).handle("persistentPty.shutdown", Effect.fn(function* () {
|
|
46
|
+
yield* pty.shutdown().pipe(mapUnavailable);
|
|
47
|
+
return HttpApiSchema.NoContent.make();
|
|
48
|
+
})).handle("persistentPty.handoff", Effect.fn(function* () {
|
|
49
|
+
return { handoff: yield* pty.handoff().pipe(mapUnavailable) };
|
|
50
|
+
})).handle("persistentPty.get", Effect.fn(function* (ctx) {
|
|
51
|
+
return { data: yield* pty.get(ctx.params.ptyID).pipe(mapTerminalError) };
|
|
52
|
+
})).handle("persistentPty.update", Effect.fn(function* (ctx) {
|
|
53
|
+
yield* pty.resize(ctx.params.ptyID, ctx.payload.size.cols, ctx.payload.size.rows, ctx.payload.attachmentID).pipe(mapTerminalError);
|
|
54
|
+
return { data: yield* pty.get(ctx.params.ptyID).pipe(mapTerminalError) };
|
|
55
|
+
})).handle("persistentPty.snapshot", Effect.fn(function* (ctx) {
|
|
56
|
+
return { data: yield* pty.snapshot(ctx.params.ptyID).pipe(mapTerminalError) };
|
|
57
|
+
})).handle("persistentPty.remove", Effect.fn(function* (ctx) {
|
|
58
|
+
yield* pty.remove(ctx.params.ptyID).pipe(mapTerminalError);
|
|
59
|
+
return HttpApiSchema.NoContent.make();
|
|
60
|
+
})).handle("persistentPty.connectToken", Effect.fn(function* (ctx) {
|
|
61
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
62
|
+
if (request.headers[PTY_CONNECT_TOKEN_HEADER] !== PTY_CONNECT_TOKEN_HEADER_VALUE || !isAllowedRequestOrigin2(request.headers.origin, request.headers.host, cors))
|
|
63
|
+
return yield* new ForbiddenError({ message: "Invalid persistent PTY connect token request" });
|
|
64
|
+
yield* pty.get(ctx.params.ptyID).pipe(mapTerminalError);
|
|
65
|
+
return { data: yield* tickets.issue({ ptyID: ctx.params.ptyID }) };
|
|
66
|
+
})).handleRaw("persistentPty.connect", Effect.fn("PersistentPtyHandler.connect")(function* (ctx) {
|
|
67
|
+
const url = new URL(ctx.request.url, "http://localhost");
|
|
68
|
+
const ticket = url.searchParams.get(PTY_CONNECT_TICKET_QUERY);
|
|
69
|
+
if (ticket) {
|
|
70
|
+
const valid = isAllowedRequestOrigin2(ctx.request.headers.origin, ctx.request.headers.host, cors) ? yield* tickets.consume({ ticket, ptyID: ctx.params.ptyID }) : false;
|
|
71
|
+
if (!valid)
|
|
72
|
+
return HttpServerResponse.empty({ status: 403 });
|
|
73
|
+
}
|
|
74
|
+
const cursor = Number(url.searchParams.get("cursor") ?? "0");
|
|
75
|
+
const role = url.searchParams.get("role") === "observer" ? "observer" : "controller";
|
|
76
|
+
const framedInput = url.searchParams.get("input_protocol") === "1";
|
|
77
|
+
const attachmentID = url.searchParams.get("attachment_id") ?? crypto.randomUUID();
|
|
78
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0)
|
|
79
|
+
return HttpServerResponse.empty({ status: 400 });
|
|
80
|
+
const socket = yield* Effect.orDie(ctx.request.upgrade);
|
|
81
|
+
const write = yield* socket.writer;
|
|
82
|
+
const outbox = yield* Queue.unbounded();
|
|
83
|
+
const input = yield* Semaphore.make(1);
|
|
84
|
+
let attachment;
|
|
85
|
+
const onOpen = Effect.gen(function* () {
|
|
86
|
+
attachment = yield* pty.attach(ctx.params.ptyID, {
|
|
87
|
+
cursor,
|
|
88
|
+
attachmentID,
|
|
89
|
+
role,
|
|
90
|
+
takeover: url.searchParams.get("takeover") === "true",
|
|
91
|
+
onEvent: (event) => {
|
|
92
|
+
if (event.type === "output")
|
|
93
|
+
Queue.offerUnsafe(outbox, event.data);
|
|
94
|
+
if (event.type === "resized")
|
|
95
|
+
Queue.offerUnsafe(outbox, JSON.stringify({ ...event, checkpoint: Buffer.from(event.checkpoint).toString("base64") }));
|
|
96
|
+
if (event.type !== "output" && event.type !== "resized")
|
|
97
|
+
Queue.offerUnsafe(outbox, JSON.stringify(event));
|
|
98
|
+
},
|
|
99
|
+
onEnd: () => Queue.offerUnsafe(outbox, new Socket.CloseEvent(1000))
|
|
100
|
+
}).pipe(Effect.catchTags({
|
|
101
|
+
"PersistentPty.NotFoundError": () => Effect.succeed(undefined),
|
|
102
|
+
"PersistentPty.UnavailableError": () => Effect.succeed(undefined)
|
|
103
|
+
}));
|
|
104
|
+
if (!attachment) {
|
|
105
|
+
Queue.offerUnsafe(outbox, new Socket.CloseEvent(4404, "terminal unavailable"));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
Queue.offerUnsafe(outbox, JSON.stringify({
|
|
109
|
+
type: "attached",
|
|
110
|
+
attachmentID,
|
|
111
|
+
inputProtocol: framedInput ? 1 : 0,
|
|
112
|
+
info: attachment.info,
|
|
113
|
+
role: attachment.role,
|
|
114
|
+
generation: attachment.generation,
|
|
115
|
+
replay: {
|
|
116
|
+
requestedOffset: attachment.replay.requestedOffset,
|
|
117
|
+
availableOffset: attachment.replay.availableOffset,
|
|
118
|
+
endOffset: attachment.replay.endOffset,
|
|
119
|
+
truncated: attachment.replay.truncated
|
|
120
|
+
}
|
|
121
|
+
}));
|
|
122
|
+
if (attachment.replay.data.length > 0)
|
|
123
|
+
Queue.offerUnsafe(outbox, attachment.replay.data);
|
|
124
|
+
Queue.offerUnsafe(outbox, JSON.stringify({ type: "replay_complete", endOffset: attachment.replay.endOffset }));
|
|
125
|
+
attachment.activate();
|
|
126
|
+
});
|
|
127
|
+
const drain = Effect.gen(function* () {
|
|
128
|
+
while (true) {
|
|
129
|
+
const item = yield* Queue.take(outbox);
|
|
130
|
+
yield* write(item);
|
|
131
|
+
if (item instanceof Socket.CloseEvent)
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
yield* runPtySocket2(drain, socket.runRaw((message) => input.withPermit(Effect.suspend(() => {
|
|
136
|
+
if (!attachment)
|
|
137
|
+
return Effect.void;
|
|
138
|
+
const data = typeof message === "string" ? Buffer.from(message) : message;
|
|
139
|
+
if (!framedInput)
|
|
140
|
+
return pty.input(ctx.params.ptyID, attachmentID, attachment.info.size.cols, attachment.info.size.rows, data).pipe(Effect.ignore);
|
|
141
|
+
if (data.byteLength < 5)
|
|
142
|
+
return Effect.void;
|
|
143
|
+
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
|
|
144
|
+
const type = data[0];
|
|
145
|
+
const cols = view.getUint16(1);
|
|
146
|
+
const rows = view.getUint16(3);
|
|
147
|
+
if (type !== 0 && type !== 1 || cols === 0 || rows === 0)
|
|
148
|
+
return Effect.void;
|
|
149
|
+
if (type === 0)
|
|
150
|
+
return pty.control(ctx.params.ptyID, attachmentID, cols, rows).pipe(Effect.ignore);
|
|
151
|
+
return pty.input(ctx.params.ptyID, attachmentID, cols, rows, data.subarray(5)).pipe(Effect.ignore);
|
|
152
|
+
})), { onOpen }), () => attachment?.detach()).pipe(Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void), Effect.orDie);
|
|
153
|
+
return HttpServerResponse.empty();
|
|
154
|
+
}));
|
|
155
|
+
}));
|
|
156
|
+
var mapUnavailable = (effect) => effect.pipe(Effect.catchTag("PersistentPty.UnavailableError", unavailable));
|
|
157
|
+
var mapTerminalError = (effect) => effect.pipe(Effect.catchTags({
|
|
158
|
+
"PersistentPty.NotFoundError": (error) => new PtyNotFoundError({ ptyID: error.ptyID, message: `PTY session not found: ${error.ptyID}` }),
|
|
159
|
+
"PersistentPty.UnavailableError": unavailable
|
|
160
|
+
}));
|
|
161
|
+
var unavailable = (error) => new ServiceUnavailableError({ message: error.message, service: "opencode-pty" });
|
|
162
|
+
|
|
163
|
+
export { PersistentPtyHandler2 };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import {
|
|
2
|
+
response2
|
|
3
|
+
} from "./workerd-6gkz39ey.js";
|
|
4
|
+
import {
|
|
5
|
+
Api2
|
|
6
|
+
} from "./workerd-z5shrcz9.js";
|
|
7
|
+
|
|
8
|
+
// src/handlers/provider.ts
|
|
9
|
+
import { Provider } from "@opencode/core/provider";
|
|
10
|
+
import { ProviderNotFoundError } from "@opencode/protocol/errors";
|
|
11
|
+
import { Effect } from "effect";
|
|
12
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
13
|
+
var ProviderHandler2 = HttpApiBuilder.group(Api2, "server.provider", (handlers) => Effect.gen(function* () {
|
|
14
|
+
return handlers.handle("provider.list", Effect.fn(function* () {
|
|
15
|
+
const providers = yield* Provider.Service;
|
|
16
|
+
return yield* response2(providers.available());
|
|
17
|
+
})).handle("provider.get", Effect.fn(function* (ctx) {
|
|
18
|
+
const providers = yield* Provider.Service;
|
|
19
|
+
const provider = yield* providers.get(ctx.params.providerID);
|
|
20
|
+
if (!provider)
|
|
21
|
+
return yield* new ProviderNotFoundError({
|
|
22
|
+
providerID: ctx.params.providerID,
|
|
23
|
+
message: `Provider not found: ${ctx.params.providerID}`
|
|
24
|
+
});
|
|
25
|
+
return yield* response2(Effect.succeed(provider));
|
|
26
|
+
}));
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
export { ProviderHandler2 };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
response2
|
|
3
|
+
} from "./workerd-6gkz39ey.js";
|
|
4
|
+
import {
|
|
5
|
+
Api2
|
|
6
|
+
} from "./workerd-z5shrcz9.js";
|
|
7
|
+
|
|
8
|
+
// src/handlers/skill.ts
|
|
9
|
+
import { Skill } from "@opencode/core/skill";
|
|
10
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
11
|
+
var SkillHandler2 = HttpApiBuilder.group(Api2, "server.skill", (handlers) => handlers.handle("skill.list", () => response2(Skill.Service.use((skill) => skill.list()))));
|
|
12
|
+
|
|
13
|
+
export { SkillHandler2 };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// src/cors.ts
|
|
2
|
+
import { Context } from "effect";
|
|
3
|
+
var opencodeOrigin = /^https:\/\/([a-z0-9-]+\.)*opencode\.ai$/;
|
|
4
|
+
var CorsConfig2 = Context.Reference("@opencode/ServerCorsConfig", {
|
|
5
|
+
defaultValue: () => {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
function isAllowedCorsOrigin2(input, opts) {
|
|
10
|
+
if (!input)
|
|
11
|
+
return true;
|
|
12
|
+
if (input.startsWith("http://localhost:"))
|
|
13
|
+
return true;
|
|
14
|
+
if (input.startsWith("http://127.0.0.1:"))
|
|
15
|
+
return true;
|
|
16
|
+
if (input.startsWith("oc://renderer"))
|
|
17
|
+
return true;
|
|
18
|
+
if (input === "tauri://localhost" || input === "http://tauri.localhost" || input === "https://tauri.localhost")
|
|
19
|
+
return true;
|
|
20
|
+
if (opencodeOrigin.test(input))
|
|
21
|
+
return true;
|
|
22
|
+
return opts?.cors?.includes(input) ?? false;
|
|
23
|
+
}
|
|
24
|
+
function isAllowedRequestOrigin2(input, host, opts) {
|
|
25
|
+
if (!input)
|
|
26
|
+
return true;
|
|
27
|
+
if (host && sameHost(input, host))
|
|
28
|
+
return true;
|
|
29
|
+
return isAllowedCorsOrigin2(input, opts);
|
|
30
|
+
}
|
|
31
|
+
function sameHost(origin, host) {
|
|
32
|
+
try {
|
|
33
|
+
return new URL(origin).host === host;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { CorsConfig2, isAllowedCorsOrigin2, isAllowedRequestOrigin2 };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Api2
|
|
3
|
+
} from "./workerd-z5shrcz9.js";
|
|
4
|
+
|
|
5
|
+
// src/handlers/config.ts
|
|
6
|
+
import { Config } from "@opencode/core/config";
|
|
7
|
+
import { ShellSelect } from "@opencode/core/shell/select";
|
|
8
|
+
import { Effect } from "effect";
|
|
9
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
10
|
+
var ConfigHandler2 = HttpApiBuilder.group(Api2, "server.config", (handlers) => handlers.handle("config.get", () => Config.Service.use((config) => config.entries())).handle("config.update", Effect.fn(function* (ctx) {
|
|
11
|
+
const config = yield* Config.Service;
|
|
12
|
+
if (!config.update)
|
|
13
|
+
return yield* Effect.die(new Error("Config updates are unavailable"));
|
|
14
|
+
return yield* config.update(ctx.payload).pipe(Effect.orDie);
|
|
15
|
+
})).handle("config.shells", Effect.fn(function* () {
|
|
16
|
+
const shell = yield* ShellSelect.Service;
|
|
17
|
+
if (!shell.list)
|
|
18
|
+
return yield* Effect.die(new Error("Shell discovery is unavailable"));
|
|
19
|
+
return yield* shell.list();
|
|
20
|
+
})));
|
|
21
|
+
|
|
22
|
+
export { ConfigHandler2 };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
response2
|
|
3
|
+
} from "./workerd-6gkz39ey.js";
|
|
4
|
+
import {
|
|
5
|
+
Api2
|
|
6
|
+
} from "./workerd-z5shrcz9.js";
|
|
7
|
+
import {
|
|
8
|
+
CorsConfig2,
|
|
9
|
+
isAllowedRequestOrigin2
|
|
10
|
+
} from "./workerd-q4bn6dy8.js";
|
|
11
|
+
import {
|
|
12
|
+
Service4
|
|
13
|
+
} from "./workerd-75c938bc.js";
|
|
14
|
+
import {
|
|
15
|
+
runPtySocket2
|
|
16
|
+
} from "./workerd-hezj1jdf.js";
|
|
17
|
+
|
|
18
|
+
// src/handlers/pty.ts
|
|
19
|
+
import { Pty } from "@opencode/core/pty";
|
|
20
|
+
import { PtyProtocol } from "@opencode/core/pty/protocol";
|
|
21
|
+
import { PtyTicket } from "@opencode/core/pty/ticket";
|
|
22
|
+
import { Location } from "@opencode/core/location";
|
|
23
|
+
import { Effect, Queue } from "effect";
|
|
24
|
+
import { HttpServerRequest, HttpServerResponse } from "effect/unstable/http";
|
|
25
|
+
import { HttpApiBuilder, HttpApiSchema } from "effect/unstable/httpapi";
|
|
26
|
+
import { Socket } from "effect/unstable/socket";
|
|
27
|
+
import { ForbiddenError, PtyNotFoundError } from "@opencode/protocol/errors";
|
|
28
|
+
import {
|
|
29
|
+
PTY_CONNECT_TICKET_QUERY,
|
|
30
|
+
PTY_CONNECT_TOKEN_HEADER,
|
|
31
|
+
PTY_CONNECT_TOKEN_HEADER_VALUE
|
|
32
|
+
} from "@opencode/protocol/groups/pty";
|
|
33
|
+
var ticketScope = Effect.gen(function* () {
|
|
34
|
+
const location = yield* Location.Service;
|
|
35
|
+
return { directory: location.directory, workspaceID: location.workspaceID };
|
|
36
|
+
});
|
|
37
|
+
var PtyHandler2 = HttpApiBuilder.group(Api2, "server.pty", (handlers) => Effect.gen(function* () {
|
|
38
|
+
const tickets = yield* PtyTicket.Service;
|
|
39
|
+
const cors = yield* CorsConfig2;
|
|
40
|
+
const environment = yield* Service4;
|
|
41
|
+
return handlers.handle("pty.list", Effect.fn(function* () {
|
|
42
|
+
const pty = yield* Pty.Service;
|
|
43
|
+
return yield* response2(pty.list());
|
|
44
|
+
})).handle("pty.create", Effect.fn(function* (ctx) {
|
|
45
|
+
const pty = yield* Pty.Service;
|
|
46
|
+
const location = yield* Location.Service;
|
|
47
|
+
const cwd = ctx.payload.cwd || location.directory;
|
|
48
|
+
return yield* response2(pty.create({
|
|
49
|
+
...ctx.payload,
|
|
50
|
+
args: ctx.payload.args ? [...ctx.payload.args] : undefined,
|
|
51
|
+
cwd,
|
|
52
|
+
env: {
|
|
53
|
+
...ctx.payload.env,
|
|
54
|
+
...yield* environment.get({ directory: location.directory, cwd })
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
})).handle("pty.get", Effect.fn(function* (ctx) {
|
|
58
|
+
const pty = yield* Pty.Service;
|
|
59
|
+
return yield* response2(pty.get(ctx.params.ptyID).pipe(Effect.catchTag("Pty.NotFoundError", () => new PtyNotFoundError({
|
|
60
|
+
ptyID: ctx.params.ptyID,
|
|
61
|
+
message: `PTY session not found: ${ctx.params.ptyID}`
|
|
62
|
+
}))));
|
|
63
|
+
})).handle("pty.update", Effect.fn(function* (ctx) {
|
|
64
|
+
const pty = yield* Pty.Service;
|
|
65
|
+
return yield* response2(pty.update(ctx.params.ptyID, {
|
|
66
|
+
...ctx.payload,
|
|
67
|
+
size: ctx.payload.size ? { ...ctx.payload.size } : undefined
|
|
68
|
+
}).pipe(Effect.catchTag("Pty.NotFoundError", () => new PtyNotFoundError({
|
|
69
|
+
ptyID: ctx.params.ptyID,
|
|
70
|
+
message: `PTY session not found: ${ctx.params.ptyID}`
|
|
71
|
+
}))));
|
|
72
|
+
})).handle("pty.remove", Effect.fn(function* (ctx) {
|
|
73
|
+
const pty = yield* Pty.Service;
|
|
74
|
+
yield* pty.remove(ctx.params.ptyID).pipe(Effect.catchTag("Pty.NotFoundError", () => new PtyNotFoundError({
|
|
75
|
+
ptyID: ctx.params.ptyID,
|
|
76
|
+
message: `PTY session not found: ${ctx.params.ptyID}`
|
|
77
|
+
})));
|
|
78
|
+
return HttpApiSchema.NoContent.make();
|
|
79
|
+
})).handle("pty.connectToken", Effect.fn(function* (ctx) {
|
|
80
|
+
const request = yield* HttpServerRequest.HttpServerRequest;
|
|
81
|
+
if (request.headers[PTY_CONNECT_TOKEN_HEADER] !== PTY_CONNECT_TOKEN_HEADER_VALUE || !isAllowedRequestOrigin2(request.headers.origin, request.headers.host, cors))
|
|
82
|
+
return yield* new ForbiddenError({ message: "Invalid PTY connect token request" });
|
|
83
|
+
const pty = yield* Pty.Service;
|
|
84
|
+
yield* pty.get(ctx.params.ptyID).pipe(Effect.catchTag("Pty.NotFoundError", () => new PtyNotFoundError({
|
|
85
|
+
ptyID: ctx.params.ptyID,
|
|
86
|
+
message: `PTY session not found: ${ctx.params.ptyID}`
|
|
87
|
+
})));
|
|
88
|
+
return yield* response2(tickets.issue({ ptyID: ctx.params.ptyID, ...yield* ticketScope }));
|
|
89
|
+
})).handleRaw("pty.connect", Effect.fn("PtyHandler.connect")(function* (ctx) {
|
|
90
|
+
const pty = yield* Pty.Service;
|
|
91
|
+
const exists = yield* pty.get(ctx.params.ptyID).pipe(Effect.as(true), Effect.catchTag("Pty.NotFoundError", () => Effect.succeed(false)));
|
|
92
|
+
if (!exists)
|
|
93
|
+
return HttpServerResponse.empty({ status: 404 });
|
|
94
|
+
const url = new URL(ctx.request.url, "http://localhost");
|
|
95
|
+
const ticket = url.searchParams.get(PTY_CONNECT_TICKET_QUERY);
|
|
96
|
+
if (ticket) {
|
|
97
|
+
const valid = isAllowedRequestOrigin2(ctx.request.headers.origin, ctx.request.headers.host, cors) ? yield* tickets.consume({ ticket, ptyID: ctx.params.ptyID, ...yield* ticketScope }) : false;
|
|
98
|
+
if (!valid)
|
|
99
|
+
return HttpServerResponse.empty({ status: 403 });
|
|
100
|
+
}
|
|
101
|
+
const parsedCursor = url.searchParams.get("cursor");
|
|
102
|
+
const cursorNumber = parsedCursor === null ? undefined : Number(parsedCursor);
|
|
103
|
+
const cursor = cursorNumber !== undefined && Number.isSafeInteger(cursorNumber) && cursorNumber >= -1 ? cursorNumber : undefined;
|
|
104
|
+
const socket = yield* Effect.orDie(ctx.request.upgrade);
|
|
105
|
+
const write = yield* socket.writer;
|
|
106
|
+
const closeAccepted = (event) => socket.runRaw(() => Effect.void, { onOpen: write(event).pipe(Effect.catch(() => Effect.void)) }).pipe(Effect.timeout("1 second"), Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void), Effect.catch(() => Effect.void));
|
|
107
|
+
const outbox = yield* Queue.unbounded();
|
|
108
|
+
const attachment = yield* pty.attach(ctx.params.ptyID, {
|
|
109
|
+
cursor,
|
|
110
|
+
onData: (chunk) => Queue.offerUnsafe(outbox, chunk),
|
|
111
|
+
onEnd: () => Queue.offerUnsafe(outbox, new Socket.CloseEvent(1000))
|
|
112
|
+
}).pipe(Effect.catchTags({
|
|
113
|
+
"Pty.NotFoundError": () => closeAccepted(new Socket.CloseEvent(4404, "session not found")).pipe(Effect.as(undefined)),
|
|
114
|
+
"Pty.ExitedError": () => closeAccepted(new Socket.CloseEvent(4404, "session exited")).pipe(Effect.as(undefined))
|
|
115
|
+
}));
|
|
116
|
+
if (!attachment)
|
|
117
|
+
return HttpServerResponse.empty();
|
|
118
|
+
for (const chunk of PtyProtocol.chunks(attachment.replay))
|
|
119
|
+
Queue.offerUnsafe(outbox, chunk);
|
|
120
|
+
Queue.offerUnsafe(outbox, PtyProtocol.metaFrame(attachment.cursor));
|
|
121
|
+
attachment.activate();
|
|
122
|
+
const drain = Effect.gen(function* () {
|
|
123
|
+
while (true) {
|
|
124
|
+
const item = yield* Queue.take(outbox);
|
|
125
|
+
yield* write(item);
|
|
126
|
+
if (item instanceof Socket.CloseEvent)
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
yield* runPtySocket2(drain, socket.runRaw((message) => {
|
|
131
|
+
const decoded = PtyProtocol.decodeInput(message);
|
|
132
|
+
if (decoded !== undefined)
|
|
133
|
+
attachment.write(decoded);
|
|
134
|
+
}), attachment.detach).pipe(Effect.catchReason("SocketError", "SocketCloseError", () => Effect.void), Effect.orDie);
|
|
135
|
+
return HttpServerResponse.empty();
|
|
136
|
+
}));
|
|
137
|
+
}));
|
|
138
|
+
|
|
139
|
+
export { PtyHandler2 };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
response2
|
|
3
|
+
} from "./workerd-6gkz39ey.js";
|
|
4
|
+
import {
|
|
5
|
+
Api2
|
|
6
|
+
} from "./workerd-z5shrcz9.js";
|
|
7
|
+
|
|
8
|
+
// src/handlers/command.ts
|
|
9
|
+
import { Command } from "@opencode/core/command";
|
|
10
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
11
|
+
var CommandHandler2 = HttpApiBuilder.group(Api2, "server.command", (handlers) => handlers.handle("command.list", () => response2(Command.Service.use((command) => command.list()))));
|
|
12
|
+
|
|
13
|
+
export { CommandHandler2 };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
missingSession2,
|
|
3
|
+
failedMessageDecode2
|
|
4
|
+
} from "./workerd-smaf4a18.js";
|
|
5
|
+
import {
|
|
6
|
+
Api2
|
|
7
|
+
} from "./workerd-z5shrcz9.js";
|
|
8
|
+
|
|
9
|
+
// src/handlers/message.ts
|
|
10
|
+
import { SessionMessage } from "@opencode/core/session/message";
|
|
11
|
+
import { Session } from "@opencode/core/session";
|
|
12
|
+
import { Effect, Schema } from "effect";
|
|
13
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
14
|
+
import { InvalidCursorError } from "@opencode/protocol/errors";
|
|
15
|
+
var DefaultMessagesLimit = 50;
|
|
16
|
+
var Cursor = Schema.Struct({
|
|
17
|
+
id: SessionMessage.ID,
|
|
18
|
+
order: Schema.Union([Schema.Literal("asc"), Schema.Literal("desc")]),
|
|
19
|
+
direction: Schema.Union([Schema.Literal("previous"), Schema.Literal("next")])
|
|
20
|
+
});
|
|
21
|
+
var decodeCursor = Schema.decodeUnknownSync(Cursor);
|
|
22
|
+
var cursor = {
|
|
23
|
+
encode(message, order, direction) {
|
|
24
|
+
return Buffer.from(JSON.stringify({ id: message.id, order, direction })).toString("base64url");
|
|
25
|
+
},
|
|
26
|
+
decode(input) {
|
|
27
|
+
return decodeCursor(JSON.parse(Buffer.from(input, "base64url").toString("utf8")));
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var MessageHandler2 = HttpApiBuilder.group(Api2, "server.message", (handlers) => Effect.gen(function* () {
|
|
31
|
+
const session = yield* Session.Service;
|
|
32
|
+
return handlers.handle("session.messages", Effect.fn(function* (ctx) {
|
|
33
|
+
if (ctx.query.cursor && ctx.query.order !== undefined)
|
|
34
|
+
return yield* new InvalidCursorError({ message: "Cursor cannot be combined with order" });
|
|
35
|
+
const decoded = yield* Effect.try({
|
|
36
|
+
try: () => ctx.query.cursor ? cursor.decode(ctx.query.cursor) : undefined,
|
|
37
|
+
catch: () => new InvalidCursorError({ message: "Invalid cursor" })
|
|
38
|
+
});
|
|
39
|
+
const order = decoded?.order ?? ctx.query.order ?? "desc";
|
|
40
|
+
const messages = yield* session.messages({
|
|
41
|
+
sessionID: ctx.params.sessionID,
|
|
42
|
+
limit: ctx.query.limit ?? DefaultMessagesLimit,
|
|
43
|
+
order,
|
|
44
|
+
type: ctx.query.type,
|
|
45
|
+
cursor: decoded ? { id: decoded.id, direction: decoded.direction } : undefined
|
|
46
|
+
}).pipe(Effect.catchTag("Session.NotFoundError", missingSession2), Effect.catchTag("Session.MessageDecodeError", failedMessageDecode2));
|
|
47
|
+
const first = messages[0];
|
|
48
|
+
const last = messages.at(-1);
|
|
49
|
+
return {
|
|
50
|
+
data: messages,
|
|
51
|
+
cursor: {
|
|
52
|
+
previous: first ? cursor.encode(first, order, "previous") : undefined,
|
|
53
|
+
next: last ? cursor.encode(last, order, "next") : undefined
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
}));
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
export { MessageHandler2 };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Api2
|
|
3
|
+
} from "./workerd-z5shrcz9.js";
|
|
4
|
+
|
|
5
|
+
// src/handlers/location.ts
|
|
6
|
+
import { Location } from "@opencode/core/location";
|
|
7
|
+
import { LocationServiceMap } from "@opencode/core/location-service-map";
|
|
8
|
+
import { ServiceUnavailableError } from "@opencode/protocol/errors";
|
|
9
|
+
import { Cause, Effect } from "effect";
|
|
10
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
11
|
+
var LocationHandler2 = HttpApiBuilder.group(Api2, "server.location", (handlers) => Effect.gen(function* () {
|
|
12
|
+
const locations = yield* LocationServiceMap.Service;
|
|
13
|
+
return handlers.handle("location.get", Effect.fn(function* () {
|
|
14
|
+
const location = yield* Location.Service;
|
|
15
|
+
return new Location.Info({
|
|
16
|
+
directory: location.directory,
|
|
17
|
+
project: location.project
|
|
18
|
+
});
|
|
19
|
+
})).handle("location.reload", () => LocationServiceMap.reload().pipe(Effect.provideService(LocationServiceMap.Service, locations), Effect.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect.failCause(cause) : Effect.fail(new ServiceUnavailableError({ message: Cause.pretty(cause), service: "location" })))));
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
export { LocationHandler2 };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// src/handlers/session-error.ts
|
|
2
|
+
import { MessageNotFoundError, SessionNotFoundError, UnknownError } from "@opencode/protocol/errors";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
function missingSession2(error) {
|
|
5
|
+
return new SessionNotFoundError({
|
|
6
|
+
sessionID: error.sessionID,
|
|
7
|
+
message: `Session not found: ${error.sessionID}`
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function missingMessage2(error) {
|
|
11
|
+
return new MessageNotFoundError({
|
|
12
|
+
sessionID: error.sessionID,
|
|
13
|
+
messageID: error.messageID,
|
|
14
|
+
message: `Message not found: ${error.messageID}`
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
function failedMessageDecode2(error) {
|
|
18
|
+
const ref = `err_${crypto.randomUUID().slice(0, 8)}`;
|
|
19
|
+
return Effect.logError("failed to decode session message").pipe(Effect.annotateLogs({ ref, sessionID: error.sessionID, messageID: error.messageID }), Effect.andThen(Effect.fail(new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }))));
|
|
20
|
+
}
|
|
21
|
+
function failedSnapshot2(operation, sessionID) {
|
|
22
|
+
return (error) => {
|
|
23
|
+
const ref = `err_${crypto.randomUUID().slice(0, 8)}`;
|
|
24
|
+
return Effect.logError(`failed to ${operation}`, { cause: error }).pipe(Effect.annotateLogs({ ref, sessionID }), Effect.andThen(Effect.fail(new UnknownError({ message: "Unexpected server error. Check server logs for details.", ref }))));
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { missingSession2, missingMessage2, failedMessageDecode2, failedSnapshot2 };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// src/middleware/schema-error.ts
|
|
2
|
+
import { Effect } from "effect";
|
|
3
|
+
import { HttpApiMiddleware } from "effect/unstable/httpapi";
|
|
4
|
+
import { InvalidRequestError } from "@opencode/protocol/errors";
|
|
5
|
+
import { SchemaErrorMiddleware } from "@opencode/protocol/middleware/schema-error";
|
|
6
|
+
import { SchemaErrorMiddleware as SchemaErrorMiddleware2 } from "@opencode/protocol/middleware/schema-error";
|
|
7
|
+
var REASON_LIMIT = 1024;
|
|
8
|
+
function truncateReason(reason) {
|
|
9
|
+
if (reason.length <= REASON_LIMIT)
|
|
10
|
+
return reason;
|
|
11
|
+
return reason.slice(0, REASON_LIMIT) + `... (${reason.length - REASON_LIMIT} more chars)`;
|
|
12
|
+
}
|
|
13
|
+
var schemaErrorLayer2 = HttpApiMiddleware.layerSchemaErrorTransform(SchemaErrorMiddleware, (error) => {
|
|
14
|
+
const reason = truncateReason(error.cause.message);
|
|
15
|
+
return Effect.logWarning("schema rejection").pipe(Effect.annotateLogs({ kind: error.kind, reason }), Effect.andThen(Effect.fail(new InvalidRequestError({ message: reason, kind: error.kind }))));
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
export { schemaErrorLayer2, SchemaErrorMiddleware2 };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Api2
|
|
3
|
+
} from "./workerd-z5shrcz9.js";
|
|
4
|
+
import {
|
|
5
|
+
Service3
|
|
6
|
+
} from "./workerd-mzr9gftb.js";
|
|
7
|
+
|
|
8
|
+
// src/handlers/server.ts
|
|
9
|
+
import { Effect } from "effect";
|
|
10
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
11
|
+
var ServerHandler2 = HttpApiBuilder.group(Api2, "server.server", (handlers) => handlers.handle("server.info", () => Effect.gen(function* () {
|
|
12
|
+
const info = yield* Service3;
|
|
13
|
+
return {
|
|
14
|
+
version: info.app.version ?? "unknown",
|
|
15
|
+
pid: process.pid ?? 0,
|
|
16
|
+
urls: info.urls(),
|
|
17
|
+
paths: info.paths
|
|
18
|
+
};
|
|
19
|
+
})));
|
|
20
|
+
|
|
21
|
+
export { ServerHandler2 };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import {
|
|
2
|
+
response2
|
|
3
|
+
} from "./workerd-6gkz39ey.js";
|
|
4
|
+
import {
|
|
5
|
+
Api2
|
|
6
|
+
} from "./workerd-z5shrcz9.js";
|
|
7
|
+
|
|
8
|
+
// src/handlers/reference.ts
|
|
9
|
+
import { Reference } from "@opencode/core/reference";
|
|
10
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
11
|
+
var ReferenceHandler2 = HttpApiBuilder.group(Api2, "server.reference", (handlers) => handlers.handle("reference.list", () => response2(Reference.Service.use((reference) => reference.list()))));
|
|
12
|
+
|
|
13
|
+
export { ReferenceHandler2 };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
LocationMiddleware2
|
|
3
|
+
} from "./workerd-6gkz39ey.js";
|
|
4
|
+
import {
|
|
5
|
+
FormLocationMiddleware2
|
|
6
|
+
} from "./workerd-5rwqcaw2.js";
|
|
7
|
+
import {
|
|
8
|
+
SessionLocationMiddleware2
|
|
9
|
+
} from "./workerd-3kzx88w9.js";
|
|
10
|
+
|
|
11
|
+
// src/api.ts
|
|
12
|
+
import { makeDefaultApi } from "@opencode/protocol/api";
|
|
13
|
+
var Api2 = makeDefaultApi({
|
|
14
|
+
locationMiddleware: LocationMiddleware2,
|
|
15
|
+
formLocationMiddleware: FormLocationMiddleware2,
|
|
16
|
+
sessionLocationMiddleware: SessionLocationMiddleware2
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export { Api2 };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Api2
|
|
3
|
+
} from "./workerd-z5shrcz9.js";
|
|
4
|
+
|
|
5
|
+
// src/handlers/project.ts
|
|
6
|
+
import { Project } from "@opencode/core/project";
|
|
7
|
+
import { Effect } from "effect";
|
|
8
|
+
import { HttpApiBuilder } from "effect/unstable/httpapi";
|
|
9
|
+
import { ProjectNotFoundError } from "@opencode/protocol/errors";
|
|
10
|
+
var ProjectHandler2 = HttpApiBuilder.group(Api2, "server.project", (handlers) => handlers.handle("project.list", () => Project.Service.use((project) => project.list())).handle("project.update", (ctx) => Project.Service.use((project) => project.update({ ...ctx.payload, projectID: ctx.params.projectID }).pipe(Effect.mapError(() => new ProjectNotFoundError({
|
|
11
|
+
projectID: ctx.params.projectID,
|
|
12
|
+
message: `Project not found: ${ctx.params.projectID}`
|
|
13
|
+
}))))));
|
|
14
|
+
|
|
15
|
+
export { ProjectHandler2 };
|