@nmzpy/pi-ember-stack 0.2.1 → 0.2.2
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 +83 -83
- package/package.json +63 -59
- package/plugins/devin-auth/extensions/index.ts +23 -0
- package/plugins/devin-auth/src/cloud-direct/auth.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/catalog.ts +246 -246
- package/plugins/devin-auth/src/cloud-direct/chat.ts +1096 -1091
- package/plugins/devin-auth/src/cloud-direct/index.ts +41 -41
- package/plugins/devin-auth/src/cloud-direct/metadata.ts +78 -78
- package/plugins/devin-auth/src/cloud-direct/wire.ts +202 -202
- package/plugins/devin-auth/src/oauth/register-user.ts +174 -174
- package/plugins/devin-auth/src/oauth/types.ts +71 -71
- package/plugins/devin-auth/src/stream.ts +1 -1
- package/plugins/pi-compact-tools/index.ts +19 -1
- package/plugins/pi-compact-tools/renderer.ts +231 -61
- package/plugins/pi-custom-agents/index.ts +310 -102
- package/plugins/pi-custom-agents/questionnaire-tool.ts +14 -5
- package/plugins/pi-custom-agents/subagent/extensions/agents.ts +18 -1
- package/plugins/pi-custom-agents/subagent/extensions/index.ts +116 -224
- package/plugins/pi-custom-agents/subagent/extensions/model.ts +96 -96
- package/plugins/pi-custom-agents/subagent/extensions/render.ts +205 -1
- package/plugins/pi-custom-agents/subagent/extensions/runner.ts +241 -177
- package/plugins/pi-custom-agents/subagent/extensions/test/render.test.ts +145 -0
- package/plugins/pi-ember-fff/index.ts +275 -17
- package/plugins/pi-ember-fff/query.ts +170 -10
- package/plugins/pi-ember-fff/test/query.test.ts +157 -1
- package/plugins/pi-ember-fff/test/renderer.test.ts +367 -16
- package/plugins/pi-ember-tps/index.ts +27 -5
- package/plugins/pi-ember-ui/ember.json +3 -0
- package/plugins/pi-ember-ui/index.ts +223 -36
- package/plugins/pi-ember-ui/mode-colors.ts +36 -8
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Public surface of the cloud-direct module.
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* import { streamChat } from './cloud-direct/index.js';
|
|
6
|
-
*
|
|
7
|
-
* for await (const delta of streamChat({
|
|
8
|
-
* apiKey: creds.apiKey,
|
|
9
|
-
* apiServerUrl: creds.apiServerUrl,
|
|
10
|
-
* modelUid: 'swe-1-6',
|
|
11
|
-
* messages: [{ role: 'user', content: 'hi' }],
|
|
12
|
-
* })) {
|
|
13
|
-
* process.stdout.write(delta);
|
|
14
|
-
* }
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
export {
|
|
18
|
-
streamChat,
|
|
19
|
-
streamChatEvents,
|
|
20
|
-
allocateCascadeId,
|
|
21
|
-
CloudChatError,
|
|
22
|
-
type CloudChatRequest,
|
|
23
|
-
type ChatHistoryItem,
|
|
24
|
-
type CloudChatEvent,
|
|
25
|
-
type ToolDef,
|
|
26
|
-
} from './chat.js';
|
|
27
|
-
|
|
28
|
-
export {
|
|
29
|
-
mintUserJwt,
|
|
30
|
-
getCachedUserJwt,
|
|
31
|
-
clearCachedUserJwt,
|
|
32
|
-
CloudAuthError,
|
|
33
|
-
} from './auth.js';
|
|
34
|
-
|
|
35
|
-
export {
|
|
36
|
-
getCachedCatalog,
|
|
37
|
-
clearCachedCatalog,
|
|
38
|
-
ModelNotAvailableError,
|
|
39
|
-
type ModelCatalogEntry,
|
|
40
|
-
type CacheEntry,
|
|
41
|
-
} from './catalog.js';
|
|
1
|
+
/**
|
|
2
|
+
* Public surface of the cloud-direct module.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* import { streamChat } from './cloud-direct/index.js';
|
|
6
|
+
*
|
|
7
|
+
* for await (const delta of streamChat({
|
|
8
|
+
* apiKey: creds.apiKey,
|
|
9
|
+
* apiServerUrl: creds.apiServerUrl,
|
|
10
|
+
* modelUid: 'swe-1-6',
|
|
11
|
+
* messages: [{ role: 'user', content: 'hi' }],
|
|
12
|
+
* })) {
|
|
13
|
+
* process.stdout.write(delta);
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export {
|
|
18
|
+
streamChat,
|
|
19
|
+
streamChatEvents,
|
|
20
|
+
allocateCascadeId,
|
|
21
|
+
CloudChatError,
|
|
22
|
+
type CloudChatRequest,
|
|
23
|
+
type ChatHistoryItem,
|
|
24
|
+
type CloudChatEvent,
|
|
25
|
+
type ToolDef,
|
|
26
|
+
} from './chat.js';
|
|
27
|
+
|
|
28
|
+
export {
|
|
29
|
+
mintUserJwt,
|
|
30
|
+
getCachedUserJwt,
|
|
31
|
+
clearCachedUserJwt,
|
|
32
|
+
CloudAuthError,
|
|
33
|
+
} from './auth.js';
|
|
34
|
+
|
|
35
|
+
export {
|
|
36
|
+
getCachedCatalog,
|
|
37
|
+
clearCachedCatalog,
|
|
38
|
+
ModelNotAvailableError,
|
|
39
|
+
type ModelCatalogEntry,
|
|
40
|
+
type CacheEntry,
|
|
41
|
+
} from './catalog.js';
|
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `exa.codeium_common_pb.Metadata` proto builder.
|
|
3
|
-
*
|
|
4
|
-
* Field numbers come from src/plugin/discovery.ts (which reads the bundled
|
|
5
|
-
* extension.js for live numbers). For cloud-direct we hard-code the canonical
|
|
6
|
-
* set of fields the LS always populates — the IDE-extracted dynamic numbers
|
|
7
|
-
* would help if Windsurf renumbers, but we don't have a way to refresh those
|
|
8
|
-
* without the bundled extension.js path being present.
|
|
9
|
-
*
|
|
10
|
-
* Captured from real LS upstream traffic via mitm reverse-proxy. See
|
|
11
|
-
* docs/CLOUD_DIRECT.md → "The exact captured request body (annotated)".
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import {
|
|
15
|
-
encodeMessage,
|
|
16
|
-
encodeString,
|
|
17
|
-
encodeTimestampBody,
|
|
18
|
-
encodeVarintField,
|
|
19
|
-
} from './wire.js';
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* extension_version + ide_version sent to the cloud. MUST be a string the
|
|
23
|
-
* cloud recognizes as a real Windsurf release — Cognition's API silently
|
|
24
|
-
* rejects unknown version strings with `failed_precondition: "an internal
|
|
25
|
-
* error occurred"`. We previously tried pulling our package.json version
|
|
26
|
-
* (e.g. "0.3.0") and the cloud rejected every request. Stays pinned to a
|
|
27
|
-
* known-good "2.0.0" until/unless someone explicitly overrides via
|
|
28
|
-
* `MetadataInput.windsurfVersion`.
|
|
29
|
-
*/
|
|
30
|
-
const WINDSURF_VERSION_STRING = '2.0.0';
|
|
31
|
-
|
|
32
|
-
export interface MetadataInput {
|
|
33
|
-
/** Persistent api_key from OAuth (`devin-session-token$<JWT>`). */
|
|
34
|
-
apiKey: string;
|
|
35
|
-
/** Fresh user_jwt from GetUserJwt — required for chat methods. */
|
|
36
|
-
userJwt?: string;
|
|
37
|
-
/** UUID — one per opencode session is fine. */
|
|
38
|
-
sessionId: string;
|
|
39
|
-
/** Monotonic, milliseconds since epoch. */
|
|
40
|
-
requestId: bigint;
|
|
41
|
-
/** UUID — one per RPC call. */
|
|
42
|
-
triggerId: string;
|
|
43
|
-
/** Optional override for the version string. Cosmetic. */
|
|
44
|
-
windsurfVersion?: string;
|
|
45
|
-
/** Optional override for the host OS string. */
|
|
46
|
-
osName?: string;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function osString(): string {
|
|
50
|
-
switch (process.platform) {
|
|
51
|
-
case 'darwin': return 'darwin';
|
|
52
|
-
case 'linux': return 'linux';
|
|
53
|
-
case 'win32': return 'windows';
|
|
54
|
-
default: return String(process.platform);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function buildMetadata(input: MetadataInput): Buffer {
|
|
59
|
-
const version = input.windsurfVersion ?? WINDSURF_VERSION_STRING;
|
|
60
|
-
const os = input.osName ?? osString();
|
|
61
|
-
const parts: Buffer[] = [
|
|
62
|
-
encodeString(1, 'windsurf'), // ide_name
|
|
63
|
-
encodeString(2, version), // extension_version
|
|
64
|
-
encodeString(3, input.apiKey), // api_key
|
|
65
|
-
encodeString(4, 'en'), // locale
|
|
66
|
-
encodeString(5, os), // os
|
|
67
|
-
encodeString(7, version), // ide_version
|
|
68
|
-
encodeVarintField(9, input.requestId), // request_id (uint64 monotonic)
|
|
69
|
-
encodeString(10, input.sessionId), // session_id
|
|
70
|
-
encodeString(12, 'windsurf'), // extension_name
|
|
71
|
-
encodeMessage(16, encodeTimestampBody()), // ls_timestamp (google.protobuf.Timestamp)
|
|
72
|
-
encodeString(25, input.triggerId), // trigger_id
|
|
73
|
-
encodeString(26, 'Unset'), // plan_name
|
|
74
|
-
encodeString(28, 'windsurf'), // ide_type
|
|
75
|
-
];
|
|
76
|
-
if (input.userJwt) parts.push(encodeString(21, input.userJwt)); // user_jwt
|
|
77
|
-
return Buffer.concat(parts);
|
|
78
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* `exa.codeium_common_pb.Metadata` proto builder.
|
|
3
|
+
*
|
|
4
|
+
* Field numbers come from src/plugin/discovery.ts (which reads the bundled
|
|
5
|
+
* extension.js for live numbers). For cloud-direct we hard-code the canonical
|
|
6
|
+
* set of fields the LS always populates — the IDE-extracted dynamic numbers
|
|
7
|
+
* would help if Windsurf renumbers, but we don't have a way to refresh those
|
|
8
|
+
* without the bundled extension.js path being present.
|
|
9
|
+
*
|
|
10
|
+
* Captured from real LS upstream traffic via mitm reverse-proxy. See
|
|
11
|
+
* docs/CLOUD_DIRECT.md → "The exact captured request body (annotated)".
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
encodeMessage,
|
|
16
|
+
encodeString,
|
|
17
|
+
encodeTimestampBody,
|
|
18
|
+
encodeVarintField,
|
|
19
|
+
} from './wire.js';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* extension_version + ide_version sent to the cloud. MUST be a string the
|
|
23
|
+
* cloud recognizes as a real Windsurf release — Cognition's API silently
|
|
24
|
+
* rejects unknown version strings with `failed_precondition: "an internal
|
|
25
|
+
* error occurred"`. We previously tried pulling our package.json version
|
|
26
|
+
* (e.g. "0.3.0") and the cloud rejected every request. Stays pinned to a
|
|
27
|
+
* known-good "2.0.0" until/unless someone explicitly overrides via
|
|
28
|
+
* `MetadataInput.windsurfVersion`.
|
|
29
|
+
*/
|
|
30
|
+
const WINDSURF_VERSION_STRING = '2.0.0';
|
|
31
|
+
|
|
32
|
+
export interface MetadataInput {
|
|
33
|
+
/** Persistent api_key from OAuth (`devin-session-token$<JWT>`). */
|
|
34
|
+
apiKey: string;
|
|
35
|
+
/** Fresh user_jwt from GetUserJwt — required for chat methods. */
|
|
36
|
+
userJwt?: string;
|
|
37
|
+
/** UUID — one per opencode session is fine. */
|
|
38
|
+
sessionId: string;
|
|
39
|
+
/** Monotonic, milliseconds since epoch. */
|
|
40
|
+
requestId: bigint;
|
|
41
|
+
/** UUID — one per RPC call. */
|
|
42
|
+
triggerId: string;
|
|
43
|
+
/** Optional override for the version string. Cosmetic. */
|
|
44
|
+
windsurfVersion?: string;
|
|
45
|
+
/** Optional override for the host OS string. */
|
|
46
|
+
osName?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function osString(): string {
|
|
50
|
+
switch (process.platform) {
|
|
51
|
+
case 'darwin': return 'darwin';
|
|
52
|
+
case 'linux': return 'linux';
|
|
53
|
+
case 'win32': return 'windows';
|
|
54
|
+
default: return String(process.platform);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildMetadata(input: MetadataInput): Buffer {
|
|
59
|
+
const version = input.windsurfVersion ?? WINDSURF_VERSION_STRING;
|
|
60
|
+
const os = input.osName ?? osString();
|
|
61
|
+
const parts: Buffer[] = [
|
|
62
|
+
encodeString(1, 'windsurf'), // ide_name
|
|
63
|
+
encodeString(2, version), // extension_version
|
|
64
|
+
encodeString(3, input.apiKey), // api_key
|
|
65
|
+
encodeString(4, 'en'), // locale
|
|
66
|
+
encodeString(5, os), // os
|
|
67
|
+
encodeString(7, version), // ide_version
|
|
68
|
+
encodeVarintField(9, input.requestId), // request_id (uint64 monotonic)
|
|
69
|
+
encodeString(10, input.sessionId), // session_id
|
|
70
|
+
encodeString(12, 'windsurf'), // extension_name
|
|
71
|
+
encodeMessage(16, encodeTimestampBody()), // ls_timestamp (google.protobuf.Timestamp)
|
|
72
|
+
encodeString(25, input.triggerId), // trigger_id
|
|
73
|
+
encodeString(26, 'Unset'), // plan_name
|
|
74
|
+
encodeString(28, 'windsurf'), // ide_type
|
|
75
|
+
];
|
|
76
|
+
if (input.userJwt) parts.push(encodeString(21, input.userJwt)); // user_jwt
|
|
77
|
+
return Buffer.concat(parts);
|
|
78
|
+
}
|
|
@@ -1,202 +1,202 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Manual protobuf + Connect-RPC streaming envelope helpers.
|
|
3
|
-
*
|
|
4
|
-
* Connect-RPC streaming wire format (HTTPS POST body):
|
|
5
|
-
* ┌─────────────┬────────────────┬──────────────┐
|
|
6
|
-
* │ flags 1byte │ length 4B BE │ payload │
|
|
7
|
-
* └─────────────┴────────────────┴──────────────┘
|
|
8
|
-
* flags bit 0x01 = payload is gzip-compressed
|
|
9
|
-
* flags bit 0x02 = end-of-stream (trailer frame — JSON {error} or empty {})
|
|
10
|
-
*
|
|
11
|
-
* All `Get*` methods on `exa.api_server_pb.ApiServerService` that the
|
|
12
|
-
* language_server calls upstream use this format, content-type
|
|
13
|
-
* `application/connect+proto`, with `Connect-Protocol-Version: 1`.
|
|
14
|
-
*
|
|
15
|
-
* Kept tiny and dependency-free — same philosophy as src/plugin/protobuf.ts.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import * as zlib from 'zlib';
|
|
19
|
-
|
|
20
|
-
// ----------------------------------------------------------------------------
|
|
21
|
-
// Proto wire encode
|
|
22
|
-
// ----------------------------------------------------------------------------
|
|
23
|
-
|
|
24
|
-
export function encodeVarint(value: number | bigint): Buffer {
|
|
25
|
-
const v0 = BigInt(value);
|
|
26
|
-
// Reject negatives at the boundary. Proto3 spec encodes signed types as
|
|
27
|
-
// 10-byte sign-extended varints; we don't support that here and the
|
|
28
|
-
// current call sites never need it (tags, lengths, request ids — all
|
|
29
|
-
// strictly positive). The old loop body would have terminated with
|
|
30
|
-
// `Number(-1n)` = -1, producing a malformed single 0xFF byte that the
|
|
31
|
-
// server would misparse silently. Throw instead so future regressions
|
|
32
|
-
// surface immediately.
|
|
33
|
-
if (v0 < 0n) {
|
|
34
|
-
throw new RangeError(`encodeVarint: negative input not supported (got ${value})`);
|
|
35
|
-
}
|
|
36
|
-
const bytes: number[] = [];
|
|
37
|
-
let v = v0;
|
|
38
|
-
while (v > 127n) {
|
|
39
|
-
bytes.push(Number(v & 0x7fn) | 0x80);
|
|
40
|
-
v >>= 7n;
|
|
41
|
-
}
|
|
42
|
-
bytes.push(Number(v));
|
|
43
|
-
return Buffer.from(bytes);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export function encodeTag(fieldNum: number, wire: number): Buffer {
|
|
47
|
-
return encodeVarint((fieldNum << 3) | wire);
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export function encodeString(fieldNum: number, s: string): Buffer {
|
|
51
|
-
const buf = Buffer.from(s, 'utf8');
|
|
52
|
-
return Buffer.concat([encodeTag(fieldNum, 2), encodeVarint(buf.length), buf]);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function encodeMessage(fieldNum: number, body: Buffer): Buffer {
|
|
56
|
-
return Buffer.concat([encodeTag(fieldNum, 2), encodeVarint(body.length), body]);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function encodeVarintField(fieldNum: number, v: number | bigint): Buffer {
|
|
60
|
-
return Buffer.concat([encodeTag(fieldNum, 0), encodeVarint(v)]);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function encodeFixed64Field(fieldNum: number, v: number): Buffer {
|
|
64
|
-
const b = Buffer.alloc(8);
|
|
65
|
-
b.writeDoubleLE(v, 0);
|
|
66
|
-
return Buffer.concat([encodeTag(fieldNum, 1), b]);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function encodeTimestampBody(): Buffer {
|
|
70
|
-
const now = Date.now();
|
|
71
|
-
const seconds = Math.floor(now / 1000);
|
|
72
|
-
const nanos = (now % 1000) * 1_000_000;
|
|
73
|
-
return Buffer.concat([
|
|
74
|
-
encodeVarintField(1, seconds),
|
|
75
|
-
nanos > 0 ? encodeVarintField(2, nanos) : Buffer.alloc(0),
|
|
76
|
-
]);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ----------------------------------------------------------------------------
|
|
80
|
-
// Proto wire decode
|
|
81
|
-
// ----------------------------------------------------------------------------
|
|
82
|
-
|
|
83
|
-
export function decodeVarint(buf: Buffer, offset: number): [bigint, number] {
|
|
84
|
-
let res = 0n;
|
|
85
|
-
let shift = 0n;
|
|
86
|
-
let i = offset;
|
|
87
|
-
while (i < buf.length) {
|
|
88
|
-
const b = buf[i++];
|
|
89
|
-
res |= BigInt(b & 0x7f) << shift;
|
|
90
|
-
if (!(b & 0x80)) return [res, i];
|
|
91
|
-
shift += 7n;
|
|
92
|
-
}
|
|
93
|
-
throw new Error('truncated varint');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export interface ProtoField {
|
|
97
|
-
num: number;
|
|
98
|
-
wire: number;
|
|
99
|
-
/** varint → bigint, fixed → 8/4 byte Buffer, length-delim → payload Buffer. */
|
|
100
|
-
value: bigint | Buffer;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function* iterFields(buf: Buffer): Generator<ProtoField> {
|
|
104
|
-
let i = 0;
|
|
105
|
-
while (i < buf.length) {
|
|
106
|
-
const [tagBig, ai] = decodeVarint(buf, i);
|
|
107
|
-
i = ai;
|
|
108
|
-
const tag = Number(tagBig);
|
|
109
|
-
const num = tag >> 3;
|
|
110
|
-
const wire = tag & 0x7;
|
|
111
|
-
if (wire === 0) {
|
|
112
|
-
const [v, bi] = decodeVarint(buf, i);
|
|
113
|
-
i = bi;
|
|
114
|
-
yield { num, wire, value: v };
|
|
115
|
-
} else if (wire === 1) {
|
|
116
|
-
// Bounds-check: a truncated frame mustn't yield a short fixed64 slice
|
|
117
|
-
// that downstream readers treat as a full 8-byte value. Stop iterating
|
|
118
|
-
// cleanly instead.
|
|
119
|
-
if (i + 8 > buf.length) return;
|
|
120
|
-
yield { num, wire, value: buf.slice(i, i + 8) };
|
|
121
|
-
i += 8;
|
|
122
|
-
} else if (wire === 2) {
|
|
123
|
-
const [n, ci] = decodeVarint(buf, i);
|
|
124
|
-
i = ci;
|
|
125
|
-
const len = Number(n);
|
|
126
|
-
// Bounds-check: when the declared length runs past the buffer, the
|
|
127
|
-
// frame is corrupt or truncated. Returning short-buffered slices to
|
|
128
|
-
// downstream parsers used to misparse silently (M12).
|
|
129
|
-
if (len < 0 || i + len > buf.length) return;
|
|
130
|
-
yield { num, wire, value: buf.slice(i, i + len) };
|
|
131
|
-
i += len;
|
|
132
|
-
} else if (wire === 5) {
|
|
133
|
-
if (i + 4 > buf.length) return;
|
|
134
|
-
yield { num, wire, value: buf.slice(i, i + 4) };
|
|
135
|
-
i += 4;
|
|
136
|
-
} else if (wire === 3 || wire === 4) {
|
|
137
|
-
// Wire types 3 (start group) and 4 (end group) are deprecated in
|
|
138
|
-
// proto3 but show up in some Codeium server-generated messages. They
|
|
139
|
-
// carry no length info; the safe behavior is to stop iterating
|
|
140
|
-
// gracefully rather than tear down the whole frame parse.
|
|
141
|
-
return;
|
|
142
|
-
} else {
|
|
143
|
-
// Unknown wire type — bail rather than misalign.
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// ----------------------------------------------------------------------------
|
|
150
|
-
// Connect-streaming envelope
|
|
151
|
-
// ----------------------------------------------------------------------------
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Wrap `body` (a serialized proto message) in a Connect-streaming envelope.
|
|
155
|
-
* If `compress` is true, gzip the payload and set the 0x01 flag.
|
|
156
|
-
*/
|
|
157
|
-
export function frameConnectStream(body: Buffer, compress = true): Buffer {
|
|
158
|
-
let payload = body;
|
|
159
|
-
let flags = 0;
|
|
160
|
-
if (compress) {
|
|
161
|
-
payload = zlib.gzipSync(body);
|
|
162
|
-
flags |= 0x01;
|
|
163
|
-
}
|
|
164
|
-
const header = Buffer.alloc(5);
|
|
165
|
-
header[0] = flags;
|
|
166
|
-
header.writeUInt32BE(payload.length, 1);
|
|
167
|
-
return Buffer.concat([header, payload]);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export interface ConnectFrame {
|
|
171
|
-
flags: number;
|
|
172
|
-
/** Decompressed payload (gzip handled here if flags & 0x01). */
|
|
173
|
-
payload: Buffer;
|
|
174
|
-
/** Frame is the trailer (end-of-stream). */
|
|
175
|
-
eos: boolean;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* Parse all Connect-streaming frames out of a response body.
|
|
180
|
-
*
|
|
181
|
-
* Returns array of decoded frames. Each frame's payload is already gzip-decoded
|
|
182
|
-
* if the compression flag was set.
|
|
183
|
-
*/
|
|
184
|
-
export function parseConnectFrames(buf: Buffer): ConnectFrame[] {
|
|
185
|
-
const out: ConnectFrame[] = [];
|
|
186
|
-
let i = 0;
|
|
187
|
-
while (i + 5 <= buf.length) {
|
|
188
|
-
const flags = buf[i];
|
|
189
|
-
const len = buf.readUInt32BE(i + 1);
|
|
190
|
-
if (i + 5 + len > buf.length) break;
|
|
191
|
-
let payload = buf.slice(i + 5, i + 5 + len);
|
|
192
|
-
if (flags & 0x01) {
|
|
193
|
-
// Compressed frame. If gunzip fails the frame is genuinely corrupt
|
|
194
|
-
// — surfacing as a thrown error beats parsing raw gzip bytes as proto
|
|
195
|
-
// (which previously produced misleading "yielded bad wire type" downstream).
|
|
196
|
-
payload = zlib.gunzipSync(payload);
|
|
197
|
-
}
|
|
198
|
-
out.push({ flags, payload, eos: (flags & 0x02) !== 0 });
|
|
199
|
-
i += 5 + len;
|
|
200
|
-
}
|
|
201
|
-
return out;
|
|
202
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Manual protobuf + Connect-RPC streaming envelope helpers.
|
|
3
|
+
*
|
|
4
|
+
* Connect-RPC streaming wire format (HTTPS POST body):
|
|
5
|
+
* ┌─────────────┬────────────────┬──────────────┐
|
|
6
|
+
* │ flags 1byte │ length 4B BE │ payload │
|
|
7
|
+
* └─────────────┴────────────────┴──────────────┘
|
|
8
|
+
* flags bit 0x01 = payload is gzip-compressed
|
|
9
|
+
* flags bit 0x02 = end-of-stream (trailer frame — JSON {error} or empty {})
|
|
10
|
+
*
|
|
11
|
+
* All `Get*` methods on `exa.api_server_pb.ApiServerService` that the
|
|
12
|
+
* language_server calls upstream use this format, content-type
|
|
13
|
+
* `application/connect+proto`, with `Connect-Protocol-Version: 1`.
|
|
14
|
+
*
|
|
15
|
+
* Kept tiny and dependency-free — same philosophy as src/plugin/protobuf.ts.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as zlib from 'zlib';
|
|
19
|
+
|
|
20
|
+
// ----------------------------------------------------------------------------
|
|
21
|
+
// Proto wire encode
|
|
22
|
+
// ----------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export function encodeVarint(value: number | bigint): Buffer {
|
|
25
|
+
const v0 = BigInt(value);
|
|
26
|
+
// Reject negatives at the boundary. Proto3 spec encodes signed types as
|
|
27
|
+
// 10-byte sign-extended varints; we don't support that here and the
|
|
28
|
+
// current call sites never need it (tags, lengths, request ids — all
|
|
29
|
+
// strictly positive). The old loop body would have terminated with
|
|
30
|
+
// `Number(-1n)` = -1, producing a malformed single 0xFF byte that the
|
|
31
|
+
// server would misparse silently. Throw instead so future regressions
|
|
32
|
+
// surface immediately.
|
|
33
|
+
if (v0 < 0n) {
|
|
34
|
+
throw new RangeError(`encodeVarint: negative input not supported (got ${value})`);
|
|
35
|
+
}
|
|
36
|
+
const bytes: number[] = [];
|
|
37
|
+
let v = v0;
|
|
38
|
+
while (v > 127n) {
|
|
39
|
+
bytes.push(Number(v & 0x7fn) | 0x80);
|
|
40
|
+
v >>= 7n;
|
|
41
|
+
}
|
|
42
|
+
bytes.push(Number(v));
|
|
43
|
+
return Buffer.from(bytes);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function encodeTag(fieldNum: number, wire: number): Buffer {
|
|
47
|
+
return encodeVarint((fieldNum << 3) | wire);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function encodeString(fieldNum: number, s: string): Buffer {
|
|
51
|
+
const buf = Buffer.from(s, 'utf8');
|
|
52
|
+
return Buffer.concat([encodeTag(fieldNum, 2), encodeVarint(buf.length), buf]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function encodeMessage(fieldNum: number, body: Buffer): Buffer {
|
|
56
|
+
return Buffer.concat([encodeTag(fieldNum, 2), encodeVarint(body.length), body]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function encodeVarintField(fieldNum: number, v: number | bigint): Buffer {
|
|
60
|
+
return Buffer.concat([encodeTag(fieldNum, 0), encodeVarint(v)]);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function encodeFixed64Field(fieldNum: number, v: number): Buffer {
|
|
64
|
+
const b = Buffer.alloc(8);
|
|
65
|
+
b.writeDoubleLE(v, 0);
|
|
66
|
+
return Buffer.concat([encodeTag(fieldNum, 1), b]);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function encodeTimestampBody(): Buffer {
|
|
70
|
+
const now = Date.now();
|
|
71
|
+
const seconds = Math.floor(now / 1000);
|
|
72
|
+
const nanos = (now % 1000) * 1_000_000;
|
|
73
|
+
return Buffer.concat([
|
|
74
|
+
encodeVarintField(1, seconds),
|
|
75
|
+
nanos > 0 ? encodeVarintField(2, nanos) : Buffer.alloc(0),
|
|
76
|
+
]);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ----------------------------------------------------------------------------
|
|
80
|
+
// Proto wire decode
|
|
81
|
+
// ----------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
export function decodeVarint(buf: Buffer, offset: number): [bigint, number] {
|
|
84
|
+
let res = 0n;
|
|
85
|
+
let shift = 0n;
|
|
86
|
+
let i = offset;
|
|
87
|
+
while (i < buf.length) {
|
|
88
|
+
const b = buf[i++];
|
|
89
|
+
res |= BigInt(b & 0x7f) << shift;
|
|
90
|
+
if (!(b & 0x80)) return [res, i];
|
|
91
|
+
shift += 7n;
|
|
92
|
+
}
|
|
93
|
+
throw new Error('truncated varint');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ProtoField {
|
|
97
|
+
num: number;
|
|
98
|
+
wire: number;
|
|
99
|
+
/** varint → bigint, fixed → 8/4 byte Buffer, length-delim → payload Buffer. */
|
|
100
|
+
value: bigint | Buffer;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function* iterFields(buf: Buffer): Generator<ProtoField> {
|
|
104
|
+
let i = 0;
|
|
105
|
+
while (i < buf.length) {
|
|
106
|
+
const [tagBig, ai] = decodeVarint(buf, i);
|
|
107
|
+
i = ai;
|
|
108
|
+
const tag = Number(tagBig);
|
|
109
|
+
const num = tag >> 3;
|
|
110
|
+
const wire = tag & 0x7;
|
|
111
|
+
if (wire === 0) {
|
|
112
|
+
const [v, bi] = decodeVarint(buf, i);
|
|
113
|
+
i = bi;
|
|
114
|
+
yield { num, wire, value: v };
|
|
115
|
+
} else if (wire === 1) {
|
|
116
|
+
// Bounds-check: a truncated frame mustn't yield a short fixed64 slice
|
|
117
|
+
// that downstream readers treat as a full 8-byte value. Stop iterating
|
|
118
|
+
// cleanly instead.
|
|
119
|
+
if (i + 8 > buf.length) return;
|
|
120
|
+
yield { num, wire, value: buf.slice(i, i + 8) };
|
|
121
|
+
i += 8;
|
|
122
|
+
} else if (wire === 2) {
|
|
123
|
+
const [n, ci] = decodeVarint(buf, i);
|
|
124
|
+
i = ci;
|
|
125
|
+
const len = Number(n);
|
|
126
|
+
// Bounds-check: when the declared length runs past the buffer, the
|
|
127
|
+
// frame is corrupt or truncated. Returning short-buffered slices to
|
|
128
|
+
// downstream parsers used to misparse silently (M12).
|
|
129
|
+
if (len < 0 || i + len > buf.length) return;
|
|
130
|
+
yield { num, wire, value: buf.slice(i, i + len) };
|
|
131
|
+
i += len;
|
|
132
|
+
} else if (wire === 5) {
|
|
133
|
+
if (i + 4 > buf.length) return;
|
|
134
|
+
yield { num, wire, value: buf.slice(i, i + 4) };
|
|
135
|
+
i += 4;
|
|
136
|
+
} else if (wire === 3 || wire === 4) {
|
|
137
|
+
// Wire types 3 (start group) and 4 (end group) are deprecated in
|
|
138
|
+
// proto3 but show up in some Codeium server-generated messages. They
|
|
139
|
+
// carry no length info; the safe behavior is to stop iterating
|
|
140
|
+
// gracefully rather than tear down the whole frame parse.
|
|
141
|
+
return;
|
|
142
|
+
} else {
|
|
143
|
+
// Unknown wire type — bail rather than misalign.
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ----------------------------------------------------------------------------
|
|
150
|
+
// Connect-streaming envelope
|
|
151
|
+
// ----------------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Wrap `body` (a serialized proto message) in a Connect-streaming envelope.
|
|
155
|
+
* If `compress` is true, gzip the payload and set the 0x01 flag.
|
|
156
|
+
*/
|
|
157
|
+
export function frameConnectStream(body: Buffer, compress = true): Buffer {
|
|
158
|
+
let payload = body;
|
|
159
|
+
let flags = 0;
|
|
160
|
+
if (compress) {
|
|
161
|
+
payload = zlib.gzipSync(body);
|
|
162
|
+
flags |= 0x01;
|
|
163
|
+
}
|
|
164
|
+
const header = Buffer.alloc(5);
|
|
165
|
+
header[0] = flags;
|
|
166
|
+
header.writeUInt32BE(payload.length, 1);
|
|
167
|
+
return Buffer.concat([header, payload]);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface ConnectFrame {
|
|
171
|
+
flags: number;
|
|
172
|
+
/** Decompressed payload (gzip handled here if flags & 0x01). */
|
|
173
|
+
payload: Buffer;
|
|
174
|
+
/** Frame is the trailer (end-of-stream). */
|
|
175
|
+
eos: boolean;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parse all Connect-streaming frames out of a response body.
|
|
180
|
+
*
|
|
181
|
+
* Returns array of decoded frames. Each frame's payload is already gzip-decoded
|
|
182
|
+
* if the compression flag was set.
|
|
183
|
+
*/
|
|
184
|
+
export function parseConnectFrames(buf: Buffer): ConnectFrame[] {
|
|
185
|
+
const out: ConnectFrame[] = [];
|
|
186
|
+
let i = 0;
|
|
187
|
+
while (i + 5 <= buf.length) {
|
|
188
|
+
const flags = buf[i];
|
|
189
|
+
const len = buf.readUInt32BE(i + 1);
|
|
190
|
+
if (i + 5 + len > buf.length) break;
|
|
191
|
+
let payload = buf.slice(i + 5, i + 5 + len);
|
|
192
|
+
if (flags & 0x01) {
|
|
193
|
+
// Compressed frame. If gunzip fails the frame is genuinely corrupt
|
|
194
|
+
// — surfacing as a thrown error beats parsing raw gzip bytes as proto
|
|
195
|
+
// (which previously produced misleading "yielded bad wire type" downstream).
|
|
196
|
+
payload = zlib.gunzipSync(payload);
|
|
197
|
+
}
|
|
198
|
+
out.push({ flags, payload, eos: (flags & 0x02) !== 0 });
|
|
199
|
+
i += 5 + len;
|
|
200
|
+
}
|
|
201
|
+
return out;
|
|
202
|
+
}
|