@glubean/port 0.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 +203 -0
- package/dist/async-queue.d.ts +7 -0
- package/dist/async-queue.d.ts.map +1 -0
- package/dist/async-queue.js +37 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/json-rpc.d.ts +31 -0
- package/dist/json-rpc.d.ts.map +1 -0
- package/dist/json-rpc.js +122 -0
- package/dist/lifecycle.d.ts +4 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +517 -0
- package/dist/options.d.ts +12 -0
- package/dist/options.d.ts.map +1 -0
- package/dist/options.js +32 -0
- package/dist/port.d.ts +4 -0
- package/dist/port.d.ts.map +1 -0
- package/dist/port.js +4 -0
- package/dist/provider-options.typecheck.d.ts +2 -0
- package/dist/provider-options.typecheck.d.ts.map +1 -0
- package/dist/provider-options.typecheck.js +29 -0
- package/dist/providers/adapter.d.ts +14 -0
- package/dist/providers/adapter.d.ts.map +1 -0
- package/dist/providers/adapter.js +1 -0
- package/dist/providers/claude.d.ts +9 -0
- package/dist/providers/claude.d.ts.map +1 -0
- package/dist/providers/claude.js +300 -0
- package/dist/providers/codex-normalizer.d.ts +7 -0
- package/dist/providers/codex-normalizer.d.ts.map +1 -0
- package/dist/providers/codex-normalizer.js +287 -0
- package/dist/providers/codex.d.ts +19 -0
- package/dist/providers/codex.d.ts.map +1 -0
- package/dist/providers/codex.js +300 -0
- package/dist/providers/common.d.ts +6 -0
- package/dist/providers/common.d.ts.map +1 -0
- package/dist/providers/common.js +67 -0
- package/dist/providers/gemini.d.ts +9 -0
- package/dist/providers/gemini.d.ts.map +1 -0
- package/dist/providers/gemini.js +378 -0
- package/dist/structured.d.ts +8 -0
- package/dist/structured.d.ts.map +1 -0
- package/dist/structured.js +127 -0
- package/dist/token-usage.d.ts +4 -0
- package/dist/token-usage.d.ts.map +1 -0
- package/dist/token-usage.js +59 -0
- package/dist/types.d.ts +407 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +35 -0
package/README.md
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Glubean Port
|
|
2
|
+
|
|
3
|
+
Glubean Port is a small runtime adapter layer for local coding agents.
|
|
4
|
+
|
|
5
|
+
The MVP exposes a neutral `session + turn + event + capability` API and includes Codex app-server, Claude Code, and Gemini ACP providers. Provider-specific runtime details are kept behind a typed adapter boundary, so callers can build their own orchestration on top without inheriting another caller's workflow concepts.
|
|
6
|
+
|
|
7
|
+
## Scope
|
|
8
|
+
|
|
9
|
+
- `@glubean/port` core types and `createPort()`.
|
|
10
|
+
- Codex app-server over line-delimited JSON-RPC on stdio.
|
|
11
|
+
- Claude Code over `--output-format stream-json`.
|
|
12
|
+
- Gemini over ACP JSON-RPC on stdio.
|
|
13
|
+
- Event normalization from provider notifications into `AgentEvent`.
|
|
14
|
+
- Token usage telemetry via `token.usage` events when the provider reports it.
|
|
15
|
+
- Structured JSON turns with optional Zod 4 validation and retry.
|
|
16
|
+
- Local lifecycle state for sessions, managed turns, cancellation, wait results,
|
|
17
|
+
and in-memory event replay with sequence cursors.
|
|
18
|
+
- Fixture tests that do not require a real Codex login or model call.
|
|
19
|
+
|
|
20
|
+
Not in this MVP:
|
|
21
|
+
|
|
22
|
+
- Caller orchestration state.
|
|
23
|
+
- Domain-specific high-level APIs.
|
|
24
|
+
|
|
25
|
+
## API Shape
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { createPort } from "@glubean/port"
|
|
29
|
+
|
|
30
|
+
const port = await createPort({
|
|
31
|
+
provider: "codex",
|
|
32
|
+
cwd: process.cwd(),
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const session = await port.sessions.create({
|
|
36
|
+
approvalPolicy: "on-request",
|
|
37
|
+
sandbox: "workspace-write",
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
for await (const event of port.turns.start({
|
|
41
|
+
sessionId: session.id,
|
|
42
|
+
input: [{ type: "text", text: "Review the current diff." }],
|
|
43
|
+
})) {
|
|
44
|
+
console.log(event)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
await port.close()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Managed lifecycle:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const session = await port.sessions.create()
|
|
54
|
+
|
|
55
|
+
const turn = await port.turns.submit({
|
|
56
|
+
sessionId: session.id,
|
|
57
|
+
input: [{ type: "text", text: "Work on the leased task." }],
|
|
58
|
+
timeoutMs: 5 * 60_000,
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
for await (const event of port.turns.stream({ ...turn, afterSeq: 0 })) {
|
|
62
|
+
console.log(event.seq, event.type)
|
|
63
|
+
if (event.type === "turn.completed") break
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = await port.turns.wait(turn)
|
|
67
|
+
console.log(result.status, result.text)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Structured output:
|
|
71
|
+
|
|
72
|
+
```ts
|
|
73
|
+
import { z } from "zod"
|
|
74
|
+
import { createPort } from "@glubean/port"
|
|
75
|
+
|
|
76
|
+
const port = await createPort({ provider: "claude", cwd: process.cwd() })
|
|
77
|
+
const session = await port.sessions.create()
|
|
78
|
+
|
|
79
|
+
const result = await port.turns.startStructured({
|
|
80
|
+
sessionId: session.id,
|
|
81
|
+
input: [{ type: "text", text: "Return { ok: true }." }],
|
|
82
|
+
schema: z.object({ ok: z.literal(true) }),
|
|
83
|
+
maxRetries: 2,
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
console.log(result.data)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Local Development
|
|
90
|
+
|
|
91
|
+
Node 24 can run the TypeScript sources directly.
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
npm test
|
|
95
|
+
npm run typecheck
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
To run live provider integrations, make sure `codex`, `claude`, and `gemini` are installed and authenticated:
|
|
99
|
+
|
|
100
|
+
```sh
|
|
101
|
+
npm run test:integration
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
To run a live Codex smoke:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
npm run smoke:codex -- "Say ok and stop."
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Design Notes
|
|
111
|
+
|
|
112
|
+
The public Port API is intentionally not a mirror of any provider:
|
|
113
|
+
|
|
114
|
+
- Codex uses `thread` and `turn`.
|
|
115
|
+
- ACP providers use `session` and `prompt`.
|
|
116
|
+
- Claude Code currently exposes a stream-json CLI shape rather than a public JSON-RPC appserver.
|
|
117
|
+
|
|
118
|
+
Port keeps provider-specific names in the adapter and exposes stable terms upward:
|
|
119
|
+
|
|
120
|
+
- `sessions`
|
|
121
|
+
- `turns`
|
|
122
|
+
- `AgentEvent`
|
|
123
|
+
- `capabilities`
|
|
124
|
+
|
|
125
|
+
Token usage is reported as a neutral `token.usage` event with normalized
|
|
126
|
+
`inputTokens`, `outputTokens`, `cachedInputTokens`, `reasoningOutputTokens`,
|
|
127
|
+
and `totalTokens` fields when available. Managed turn status and wait results
|
|
128
|
+
also expose the latest `usage` snapshot for the turn.
|
|
129
|
+
|
|
130
|
+
Structured output is implemented above the provider adapters. Codex and Claude Code use native JSON Schema support; Gemini currently uses prompt constraints plus local validation and retry.
|
|
131
|
+
|
|
132
|
+
Runtime options are split into provider-neutral fields and provider-specific fields. The selected provider binds the options type:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
await createPort({
|
|
136
|
+
provider: "codex",
|
|
137
|
+
options: {
|
|
138
|
+
model: "gpt-5.5",
|
|
139
|
+
effort: "high",
|
|
140
|
+
sandbox: "workspace-write",
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
await createPort({
|
|
145
|
+
provider: "claude",
|
|
146
|
+
options: {
|
|
147
|
+
model: "sonnet",
|
|
148
|
+
permissionMode: "plan",
|
|
149
|
+
thinking: { type: "adaptive" },
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
await createPort({
|
|
154
|
+
provider: "gemini",
|
|
155
|
+
options: {
|
|
156
|
+
model: "gemini-2.5-pro",
|
|
157
|
+
approvalMode: "plan",
|
|
158
|
+
settingsPath: "/path/to/gemini-settings.json",
|
|
159
|
+
},
|
|
160
|
+
})
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Lifecycle state is also implemented above the provider adapters. Providers only
|
|
164
|
+
need to expose create/resume/start/cancel primitives; Port records local
|
|
165
|
+
session and turn status, stores an in-memory event log, and exposes replay via
|
|
166
|
+
`events({ afterSeq })` or `turns.stream({ sessionId, turnId, afterSeq })`.
|
|
167
|
+
This is intentionally runtime-level only: workflow envelopes, policy labels,
|
|
168
|
+
ack/result protocols, and durable controller state stay above Port.
|
|
169
|
+
|
|
170
|
+
## Type sync with consumers
|
|
171
|
+
|
|
172
|
+
Port now ships a built artefact set: `dist/index.js` (JS) and
|
|
173
|
+
`dist/index.d.ts` (full type declarations). `package.json#exports`
|
|
174
|
+
points consumers at those, not at `src/`. Most consumers can therefore
|
|
175
|
+
import Port's types directly — no shim needed — once they install the
|
|
176
|
+
current packed tgz.
|
|
177
|
+
|
|
178
|
+
For consumers still pinned to an older tgz (or `file:` link from
|
|
179
|
+
before the dist build), a compile-time shim of the export surface
|
|
180
|
+
still exists. The current shim of record:
|
|
181
|
+
|
|
182
|
+
- `gloop` → `gloop/src/types/glubean-port.d.ts` (narrow ambient
|
|
183
|
+
module declaring only the symbols `gloop`'s reviewer adapters
|
|
184
|
+
consume: `createPort`, `Port`, `RuntimeOptions`,
|
|
185
|
+
`RuntimeOptionsFor<P>`, `StartStructuredTurnInput`,
|
|
186
|
+
`StructuredTurnResult`, `StructuredTurnError`, the per-provider
|
|
187
|
+
enums like `ClaudePermissionMode` / `GeminiApprovalMode`, etc.)
|
|
188
|
+
|
|
189
|
+
While the shim is in use, when you change the exported type surface
|
|
190
|
+
in `src/index.ts` / `src/types.ts` — adding a field to
|
|
191
|
+
`RuntimeOptions`, a new `StructuredTurnError` subclass, an additional
|
|
192
|
+
provider id, anything that affects what `import type` consumers see —
|
|
193
|
+
update the consumer's shim in the same change so the shim stays a
|
|
194
|
+
strict subset of the dist declaration. TypeScript merges the two; the
|
|
195
|
+
shim must NEVER tighten a field declared in the dist (only widen or
|
|
196
|
+
add). Skipping the update leaves the consumer's `tsc` green against a
|
|
197
|
+
stale view of Port's types and the divergence surfaces only at
|
|
198
|
+
runtime.
|
|
199
|
+
|
|
200
|
+
Removing a consumer's shim is a separate change: rebuild the
|
|
201
|
+
consumer's lockfile against a tgz that ships dist, verify `tsc`
|
|
202
|
+
greenness directly against `dist/index.d.ts`, then delete the shim
|
|
203
|
+
file in a follow-up commit.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"async-queue.d.ts","sourceRoot":"","sources":["../src/async-queue.ts"],"names":[],"mappings":"AAAA,qBAAa,UAAU,CAAC,CAAC,CAAE,YAAW,aAAa,CAAC,CAAC,CAAC;;IAKpD,IAAI,CAAC,IAAI,EAAE,CAAC,GAAG,IAAI;IAUnB,KAAK,IAAI,IAAI;IAQb,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,aAAa,CAAC,CAAC,CAAC;CAc3C"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export class AsyncQueue {
|
|
2
|
+
#items = [];
|
|
3
|
+
#waiters = [];
|
|
4
|
+
#closed = false;
|
|
5
|
+
push(item) {
|
|
6
|
+
if (this.#closed)
|
|
7
|
+
return;
|
|
8
|
+
const waiter = this.#waiters.shift();
|
|
9
|
+
if (waiter) {
|
|
10
|
+
waiter({ value: item, done: false });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
this.#items.push(item);
|
|
14
|
+
}
|
|
15
|
+
close() {
|
|
16
|
+
if (this.#closed)
|
|
17
|
+
return;
|
|
18
|
+
this.#closed = true;
|
|
19
|
+
for (const waiter of this.#waiters.splice(0)) {
|
|
20
|
+
waiter({ value: undefined, done: true });
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
[Symbol.asyncIterator]() {
|
|
24
|
+
return {
|
|
25
|
+
next: () => {
|
|
26
|
+
const item = this.#items.shift();
|
|
27
|
+
if (item !== undefined) {
|
|
28
|
+
return Promise.resolve({ value: item, done: false });
|
|
29
|
+
}
|
|
30
|
+
if (this.#closed) {
|
|
31
|
+
return Promise.resolve({ value: undefined, done: true });
|
|
32
|
+
}
|
|
33
|
+
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { CreatePortInput, CreatePortInputFor, Port } from "./types.ts";
|
|
2
|
+
export * from "./types.ts";
|
|
3
|
+
export { CodexEventNormalizer } from "./providers/codex-normalizer.ts";
|
|
4
|
+
export { codexCapabilities, createCodexAdapter, createCodexPort, createCodexThreadParams } from "./providers/codex.ts";
|
|
5
|
+
export { claudeCapabilities, createClaudeAdapter, createClaudePort } from "./providers/claude.ts";
|
|
6
|
+
export { geminiCapabilities, createGeminiAdapter, createGeminiPort } from "./providers/gemini.ts";
|
|
7
|
+
export type { ProviderAdapter } from "./providers/adapter.ts";
|
|
8
|
+
export { JsonRpcLineConnection } from "./json-rpc.ts";
|
|
9
|
+
export { StructuredTurnError } from "./structured.ts";
|
|
10
|
+
export declare function createPort(input: CreatePortInputFor<"codex">): Promise<Port<"codex">>;
|
|
11
|
+
export declare function createPort(input: CreatePortInputFor<"claude">): Promise<Port<"claude">>;
|
|
12
|
+
export declare function createPort(input: CreatePortInputFor<"gemini">): Promise<Port<"gemini">>;
|
|
13
|
+
export declare function createPort(input: CreatePortInput): Promise<Port>;
|
|
14
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,kBAAkB,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAK5E,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AACvE,OAAO,EAAE,iBAAiB,EAAE,kBAAkB,EAAE,eAAe,EAAE,uBAAuB,EAAE,MAAM,sBAAsB,CAAC;AACvH,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAClG,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAClG,YAAY,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,eAAe,CAAC;AACtD,OAAO,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAEtD,wBAAsB,UAAU,CAAC,KAAK,EAAE,kBAAkB,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;AAC7F,wBAAsB,UAAU,CAAC,KAAK,EAAE,kBAAkB,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC/F,wBAAsB,UAAU,CAAC,KAAK,EAAE,kBAAkB,CAAC,QAAQ,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC,CAAC;AAC/F,wBAAsB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createCodexPort } from "./providers/codex.js";
|
|
2
|
+
import { createClaudePort } from "./providers/claude.js";
|
|
3
|
+
import { createGeminiPort } from "./providers/gemini.js";
|
|
4
|
+
export * from "./types.js";
|
|
5
|
+
export { CodexEventNormalizer } from "./providers/codex-normalizer.js";
|
|
6
|
+
export { codexCapabilities, createCodexAdapter, createCodexPort, createCodexThreadParams } from "./providers/codex.js";
|
|
7
|
+
export { claudeCapabilities, createClaudeAdapter, createClaudePort } from "./providers/claude.js";
|
|
8
|
+
export { geminiCapabilities, createGeminiAdapter, createGeminiPort } from "./providers/gemini.js";
|
|
9
|
+
export { JsonRpcLineConnection } from "./json-rpc.js";
|
|
10
|
+
export { StructuredTurnError } from "./structured.js";
|
|
11
|
+
export async function createPort(input) {
|
|
12
|
+
switch (input.provider) {
|
|
13
|
+
case "codex":
|
|
14
|
+
return createCodexPort({ ...input, provider: "codex" });
|
|
15
|
+
case "claude":
|
|
16
|
+
return createClaudePort({ ...input, provider: "claude" });
|
|
17
|
+
case "gemini":
|
|
18
|
+
return createGeminiPort({ ...input, provider: "gemini" });
|
|
19
|
+
default:
|
|
20
|
+
throw new Error(`Unsupported provider: ${input.provider}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import type { JsonValue } from "./types.ts";
|
|
3
|
+
export type JsonRpcId = string | number | null;
|
|
4
|
+
export type JsonRpcMessage = {
|
|
5
|
+
jsonrpc?: "2.0";
|
|
6
|
+
id?: JsonRpcId;
|
|
7
|
+
method?: string;
|
|
8
|
+
params?: JsonValue;
|
|
9
|
+
result?: JsonValue;
|
|
10
|
+
error?: {
|
|
11
|
+
code?: number;
|
|
12
|
+
message?: string;
|
|
13
|
+
data?: JsonValue;
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
export type JsonRpcNotification = {
|
|
17
|
+
method: string;
|
|
18
|
+
params?: JsonValue;
|
|
19
|
+
};
|
|
20
|
+
type RequestListener = (request: JsonRpcNotification) => JsonValue | undefined | Promise<JsonValue | undefined>;
|
|
21
|
+
export declare class JsonRpcLineConnection {
|
|
22
|
+
#private;
|
|
23
|
+
constructor(input: Readable, output: Writable);
|
|
24
|
+
onNotification(listener: (notification: JsonRpcNotification) => void): () => void;
|
|
25
|
+
onRequest(listener: RequestListener): () => void;
|
|
26
|
+
request(method: string, params?: JsonValue): Promise<JsonValue | undefined>;
|
|
27
|
+
notify(method: string, params?: JsonValue): void;
|
|
28
|
+
close(): void;
|
|
29
|
+
}
|
|
30
|
+
export {};
|
|
31
|
+
//# sourceMappingURL=json-rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"json-rpc.d.ts","sourceRoot":"","sources":["../src/json-rpc.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AACtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAE5C,MAAM,MAAM,SAAS,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;AAE/C,MAAM,MAAM,cAAc,GAAG;IAC3B,OAAO,CAAC,EAAE,KAAK,CAAC;IAChB,EAAE,CAAC,EAAE,SAAS,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,KAAK,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,SAAS,CAAA;KAAE,CAAC;CAC/D,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,SAAS,CAAC;CACpB,CAAC;AAOF,KAAK,eAAe,GAAG,CAAC,OAAO,EAAE,mBAAmB,KAAK,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAAC;AAEhH,qBAAa,qBAAqB;;gBAQpB,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ;IAS7C,cAAc,CAAC,QAAQ,EAAE,CAAC,YAAY,EAAE,mBAAmB,KAAK,IAAI,GAAG,MAAM,IAAI;IAKjF,SAAS,CAAC,QAAQ,EAAE,eAAe,GAAG,MAAM,IAAI;IAKhD,OAAO,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC;IAS3E,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,SAAS,GAAG,IAAI;IAIhD,KAAK,IAAI,IAAI;CA0Fd"}
|
package/dist/json-rpc.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { EventEmitter } from "node:events";
|
|
2
|
+
export class JsonRpcLineConnection {
|
|
3
|
+
#input;
|
|
4
|
+
#output;
|
|
5
|
+
#nextId = 0;
|
|
6
|
+
#buffer = "";
|
|
7
|
+
#pending = new Map();
|
|
8
|
+
#events = new EventEmitter();
|
|
9
|
+
constructor(input, output) {
|
|
10
|
+
this.#input = input;
|
|
11
|
+
this.#output = output;
|
|
12
|
+
this.#input.setEncoding("utf8");
|
|
13
|
+
this.#input.on("data", (chunk) => this.#acceptChunk(chunk));
|
|
14
|
+
this.#input.on("error", (error) => this.#rejectAll(error));
|
|
15
|
+
this.#input.on("close", () => this.#rejectAll(new Error("JSON-RPC input closed")));
|
|
16
|
+
}
|
|
17
|
+
onNotification(listener) {
|
|
18
|
+
this.#events.on("notification", listener);
|
|
19
|
+
return () => this.#events.off("notification", listener);
|
|
20
|
+
}
|
|
21
|
+
onRequest(listener) {
|
|
22
|
+
this.#events.on("request", listener);
|
|
23
|
+
return () => this.#events.off("request", listener);
|
|
24
|
+
}
|
|
25
|
+
request(method, params) {
|
|
26
|
+
const id = ++this.#nextId;
|
|
27
|
+
const message = { jsonrpc: "2.0", id, method, params };
|
|
28
|
+
this.#write(message);
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
this.#pending.set(id, { resolve, reject });
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
notify(method, params) {
|
|
34
|
+
this.#write({ jsonrpc: "2.0", method, params });
|
|
35
|
+
}
|
|
36
|
+
close() {
|
|
37
|
+
this.#rejectAll(new Error("JSON-RPC connection closed"));
|
|
38
|
+
this.#input.destroy();
|
|
39
|
+
this.#output.end();
|
|
40
|
+
}
|
|
41
|
+
#write(message) {
|
|
42
|
+
this.#output.write(`${JSON.stringify(message)}\n`);
|
|
43
|
+
}
|
|
44
|
+
#acceptChunk(chunk) {
|
|
45
|
+
this.#buffer += chunk;
|
|
46
|
+
while (true) {
|
|
47
|
+
const newlineIndex = this.#buffer.indexOf("\n");
|
|
48
|
+
if (newlineIndex === -1)
|
|
49
|
+
break;
|
|
50
|
+
const line = this.#buffer.slice(0, newlineIndex).trim();
|
|
51
|
+
this.#buffer = this.#buffer.slice(newlineIndex + 1);
|
|
52
|
+
if (line)
|
|
53
|
+
this.#acceptLine(line);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
#acceptLine(line) {
|
|
57
|
+
let message;
|
|
58
|
+
try {
|
|
59
|
+
message = JSON.parse(line);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
this.#events.emit("notification", {
|
|
63
|
+
method: "port/invalid-json",
|
|
64
|
+
params: { line },
|
|
65
|
+
});
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (message.id !== undefined && (message.result !== undefined || message.error !== undefined)) {
|
|
69
|
+
const pending = this.#pending.get(message.id);
|
|
70
|
+
if (!pending)
|
|
71
|
+
return;
|
|
72
|
+
this.#pending.delete(message.id);
|
|
73
|
+
if (message.error) {
|
|
74
|
+
pending.reject(new Error(message.error.message ?? "JSON-RPC request failed"));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
pending.resolve(message.result);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (message.id !== undefined && typeof message.method === "string") {
|
|
81
|
+
void this.#acceptRequest(message.id, message.method, message.params);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
if (typeof message.method === "string") {
|
|
85
|
+
this.#events.emit("notification", {
|
|
86
|
+
method: message.method,
|
|
87
|
+
params: message.params,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async #acceptRequest(id, method, params) {
|
|
92
|
+
const listeners = this.#events.listeners("request");
|
|
93
|
+
if (listeners.length === 0) {
|
|
94
|
+
this.#write({
|
|
95
|
+
jsonrpc: "2.0",
|
|
96
|
+
id,
|
|
97
|
+
error: { code: -32601, message: `Method not found: ${method}` },
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
const result = await listeners[0]({ method, params });
|
|
103
|
+
this.#write({ jsonrpc: "2.0", id, result });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
this.#write({
|
|
107
|
+
jsonrpc: "2.0",
|
|
108
|
+
id,
|
|
109
|
+
error: {
|
|
110
|
+
code: -32000,
|
|
111
|
+
message: error instanceof Error ? error.message : String(error),
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
#rejectAll(error) {
|
|
117
|
+
for (const pending of this.#pending.values()) {
|
|
118
|
+
pending.reject(error);
|
|
119
|
+
}
|
|
120
|
+
this.#pending.clear();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D,OAAO,KAAK,EAMV,IAAI,EACJ,UAAU,EAkBX,MAAM,YAAY,CAAC;AAiCpB,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,CAAC,CAE9F"}
|