@buyong/pi-devin 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +214 -2
- package/index.ts +16 -0
- package/package.json +62 -3
- package/proto/README.md +78 -0
- package/proto/buf/validate/validate.proto +453 -0
- package/proto/cel/expr/checked.proto +109 -0
- package/proto/cel/expr/eval.proto +44 -0
- package/proto/cel/expr/explain.proto +22 -0
- package/proto/cel/expr/syntax.proto +119 -0
- package/proto/cel/expr/value.proto +47 -0
- package/proto/connectext/grpc/status/v1/status.proto +21 -0
- package/proto/errorspb/errors.proto +58 -0
- package/proto/errorspb/hintdetail.proto +9 -0
- package/proto/errorspb/markers.proto +12 -0
- package/proto/errorspb/tags.proto +14 -0
- package/proto/errorspb/testing.proto +8 -0
- package/proto/exa/analytics_pb/analytics.proto +190 -0
- package/proto/exa/api_server_pb/api_server.proto +2590 -0
- package/proto/exa/auth_pb/auth.proto +21 -0
- package/proto/exa/auto_cascade_common_pb/auto_cascade_common.proto +81 -0
- package/proto/exa/browser_preview_pb/browser_preview.proto +26 -0
- package/proto/exa/bug_checker_pb/bug_checker.proto +24 -0
- package/proto/exa/cascade_plugins_pb/cascade_plugins.proto +264 -0
- package/proto/exa/chat_client_server_pb/chat_client_server.proto +57 -0
- package/proto/exa/chat_pb/chat.proto +452 -0
- package/proto/exa/code_edit/code_edit_pb/code_edit.proto +186 -0
- package/proto/exa/codeium_common_pb/browser_context.proto +31 -0
- package/proto/exa/codeium_common_pb/codeium_common.proto +4183 -0
- package/proto/exa/context_module_pb/context_module.proto +174 -0
- package/proto/exa/cortex_pb/cortex.proto +3288 -0
- package/proto/exa/dev_pb/dev.proto +28 -0
- package/proto/exa/diff_action_pb/diff_action.proto +76 -0
- package/proto/exa/eval/pr_eval/datasets_pb/datasets.proto +106 -0
- package/proto/exa/eval_pb/eval.proto +1318 -0
- package/proto/exa/extension_server_pb/extension_server.proto +555 -0
- package/proto/exa/file_system_provider_pb/file_system_provider.proto +75 -0
- package/proto/exa/index_pb/index.proto +469 -0
- package/proto/exa/knowledge_base_pb/knowledge_base.proto +146 -0
- package/proto/exa/language_server_pb/language_server.proto +2435 -0
- package/proto/exa/model_management_pb/model_management.proto +184 -0
- package/proto/exa/opensearch_clients_pb/opensearch_clients.proto +495 -0
- package/proto/exa/product_analytics_pb/product_analytics.proto +39 -0
- package/proto/exa/prompt_pb/prompt.proto +97 -0
- package/proto/exa/reactive_component_pb/reactive_component.proto +98 -0
- package/proto/exa/seat_management_pb/seat_management.proto +2910 -0
- package/proto/exa/tokenizer_pb/tokenizer.proto +39 -0
- package/proto/exa/trainer_pb/config.proto +643 -0
- package/proto/exa/tree_sitter/language_data_pb/language_data.proto +14 -0
- package/proto/exa/trust_pb/trust.proto +192 -0
- package/proto/exa/user_analytics_pb/user_analytics.proto +752 -0
- package/proto/google/api/expr/v1alpha1/checked.proto +108 -0
- package/proto/google/api/expr/v1alpha1/eval.proto +38 -0
- package/proto/google/api/expr/v1alpha1/explain.proto +22 -0
- package/proto/google/api/expr/v1alpha1/syntax.proto +126 -0
- package/proto/google/api/expr/v1alpha1/value.proto +47 -0
- package/proto/google/protobuf/any.proto +15 -0
- package/proto/google/protobuf/cpp_features.proto +22 -0
- package/proto/google/protobuf/descriptor.proto +473 -0
- package/proto/google/protobuf/duration.proto +16 -0
- package/proto/google/protobuf/empty.proto +14 -0
- package/proto/google/protobuf/go_features.proto +31 -0
- package/proto/google/protobuf/struct.proto +34 -0
- package/proto/google/protobuf/timestamp.proto +16 -0
- package/proto/google/protobuf/wrappers.proto +47 -0
- package/proto/google/rpc/status.proto +18 -0
- package/proto/grpc/binlog/v1/binarylog.proto +89 -0
- package/proto/io/prometheus/client/metrics.proto +103 -0
- package/src/core/auth/access.ts +6 -0
- package/src/core/auth/index.ts +2 -0
- package/src/core/auth/login.ts +177 -0
- package/src/core/auth/rest-exchange.ts +35 -0
- package/src/core/client.ts +153 -0
- package/src/core/index.ts +12 -0
- package/src/core/inference/chat.ts +229 -0
- package/src/core/inference/images.ts +24 -0
- package/src/core/inference/messages.ts +34 -0
- package/src/core/inference/retry.ts +68 -0
- package/src/core/inference/state.ts +8 -0
- package/src/core/models/access.ts +7 -0
- package/src/core/models/catalog.ts +35 -0
- package/src/core/models/index.ts +4 -0
- package/src/core/models/select.ts +43 -0
- package/src/core/models/variants.ts +163 -0
- package/src/core/protocol/device.ts +73 -0
- package/src/core/protocol/fingerprint.ts +59 -0
- package/src/core/protocol/http.ts +109 -0
- package/src/core/protocol/protobuf.ts +112 -0
- package/src/core/protocol/transport.ts +501 -0
- package/src/core/types.ts +115 -0
- package/src/pi/auth.ts +134 -0
- package/src/pi/devin-command.ts +76 -0
- package/src/pi/devin-panel.ts +77 -0
- package/src/pi/devin-settings.ts +50 -0
- package/src/pi/extension.ts +80 -0
- package/src/pi/fusion-command.ts +83 -0
- package/src/pi/fusion-picker.ts +171 -0
- package/src/pi/fusion-settings.ts +44 -0
- package/src/pi/lifecycle.ts +217 -0
- package/src/pi/messages.ts +107 -0
- package/src/pi/models.ts +378 -0
- package/src/pi/provider.ts +241 -0
- package/src/pi/sidekick-report.ts +42 -0
- package/src/pi/sidekick-runtime.ts +287 -0
- package/src/pi/sidekick.ts +190 -0
- package/src/pi/stream.ts +124 -0
- package/src/pi/usage.ts +153 -0
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
syntax = "proto3";
|
|
2
|
+
|
|
3
|
+
package grpc.binarylog.v1;
|
|
4
|
+
|
|
5
|
+
import "google/protobuf/duration.proto";
|
|
6
|
+
import "google/protobuf/timestamp.proto";
|
|
7
|
+
|
|
8
|
+
option java_package = "io.grpc.binarylog.v1";
|
|
9
|
+
option java_outer_classname = "BinaryLogProto";
|
|
10
|
+
option java_multiple_files = true;
|
|
11
|
+
option go_package = "google.golang.org/grpc/binarylog/grpc_binarylog_v1";
|
|
12
|
+
|
|
13
|
+
message GrpcLogEntry {
|
|
14
|
+
.google.protobuf.Timestamp timestamp = 1;
|
|
15
|
+
uint64 call_id = 2;
|
|
16
|
+
uint64 sequence_id_within_call = 3;
|
|
17
|
+
GrpcLogEntry.EventType type = 4;
|
|
18
|
+
GrpcLogEntry.Logger logger = 5;
|
|
19
|
+
oneof payload {
|
|
20
|
+
ClientHeader client_header = 6;
|
|
21
|
+
ServerHeader server_header = 7;
|
|
22
|
+
Message message = 8;
|
|
23
|
+
Trailer trailer = 9;
|
|
24
|
+
}
|
|
25
|
+
bool payload_truncated = 10;
|
|
26
|
+
Address peer = 11;
|
|
27
|
+
enum EventType {
|
|
28
|
+
EVENT_TYPE_UNKNOWN = 0;
|
|
29
|
+
EVENT_TYPE_CLIENT_HEADER = 1;
|
|
30
|
+
EVENT_TYPE_SERVER_HEADER = 2;
|
|
31
|
+
EVENT_TYPE_CLIENT_MESSAGE = 3;
|
|
32
|
+
EVENT_TYPE_SERVER_MESSAGE = 4;
|
|
33
|
+
EVENT_TYPE_CLIENT_HALF_CLOSE = 5;
|
|
34
|
+
EVENT_TYPE_SERVER_TRAILER = 6;
|
|
35
|
+
EVENT_TYPE_CANCEL = 7;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
enum Logger {
|
|
39
|
+
LOGGER_UNKNOWN = 0;
|
|
40
|
+
LOGGER_CLIENT = 1;
|
|
41
|
+
LOGGER_SERVER = 2;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
message ClientHeader {
|
|
47
|
+
Metadata metadata = 1;
|
|
48
|
+
string method_name = 2;
|
|
49
|
+
string authority = 3;
|
|
50
|
+
.google.protobuf.Duration timeout = 4;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
message ServerHeader {
|
|
54
|
+
Metadata metadata = 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
message Trailer {
|
|
58
|
+
Metadata metadata = 1;
|
|
59
|
+
uint32 status_code = 2;
|
|
60
|
+
string status_message = 3;
|
|
61
|
+
bytes status_details = 4;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
message Message {
|
|
65
|
+
uint32 length = 1;
|
|
66
|
+
bytes data = 2;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
message Metadata {
|
|
70
|
+
repeated MetadataEntry entry = 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
message MetadataEntry {
|
|
74
|
+
string key = 1;
|
|
75
|
+
bytes value = 2;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
message Address {
|
|
79
|
+
Address.Type type = 1;
|
|
80
|
+
string address = 2;
|
|
81
|
+
uint32 ip_port = 3;
|
|
82
|
+
enum Type {
|
|
83
|
+
TYPE_UNKNOWN = 0;
|
|
84
|
+
TYPE_IPV4 = 1;
|
|
85
|
+
TYPE_IPV6 = 2;
|
|
86
|
+
TYPE_UNIX = 3;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
syntax = "proto2";
|
|
2
|
+
|
|
3
|
+
package io.prometheus.client;
|
|
4
|
+
|
|
5
|
+
import "google/protobuf/timestamp.proto";
|
|
6
|
+
|
|
7
|
+
option java_package = "io.prometheus.client";
|
|
8
|
+
option go_package = "github.com/prometheus/client_model/go;io_prometheus_client";
|
|
9
|
+
|
|
10
|
+
enum MetricType {
|
|
11
|
+
COUNTER = 0;
|
|
12
|
+
GAUGE = 1;
|
|
13
|
+
SUMMARY = 2;
|
|
14
|
+
UNTYPED = 3;
|
|
15
|
+
HISTOGRAM = 4;
|
|
16
|
+
GAUGE_HISTOGRAM = 5;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
message LabelPair {
|
|
20
|
+
optional string name = 1;
|
|
21
|
+
optional string value = 2;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
message Gauge {
|
|
25
|
+
optional double value = 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
message Counter {
|
|
29
|
+
optional double value = 1;
|
|
30
|
+
optional Exemplar exemplar = 2;
|
|
31
|
+
optional .google.protobuf.Timestamp created_timestamp = 3;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
message Quantile {
|
|
35
|
+
optional double quantile = 1;
|
|
36
|
+
optional double value = 2;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
message Summary {
|
|
40
|
+
optional uint64 sample_count = 1;
|
|
41
|
+
optional double sample_sum = 2;
|
|
42
|
+
repeated Quantile quantile = 3;
|
|
43
|
+
optional .google.protobuf.Timestamp created_timestamp = 4;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
message Untyped {
|
|
47
|
+
optional double value = 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
message Histogram {
|
|
51
|
+
optional uint64 sample_count = 1;
|
|
52
|
+
optional double sample_count_float = 4;
|
|
53
|
+
optional double sample_sum = 2;
|
|
54
|
+
repeated Bucket bucket = 3;
|
|
55
|
+
optional .google.protobuf.Timestamp created_timestamp = 15;
|
|
56
|
+
optional sint32 schema = 5;
|
|
57
|
+
optional double zero_threshold = 6;
|
|
58
|
+
optional uint64 zero_count = 7;
|
|
59
|
+
optional double zero_count_float = 8;
|
|
60
|
+
repeated BucketSpan negative_span = 9;
|
|
61
|
+
repeated sint64 negative_delta = 10;
|
|
62
|
+
repeated double negative_count = 11;
|
|
63
|
+
repeated BucketSpan positive_span = 12;
|
|
64
|
+
repeated sint64 positive_delta = 13;
|
|
65
|
+
repeated double positive_count = 14;
|
|
66
|
+
repeated Exemplar exemplars = 16;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
message Bucket {
|
|
70
|
+
optional uint64 cumulative_count = 1;
|
|
71
|
+
optional double cumulative_count_float = 4;
|
|
72
|
+
optional double upper_bound = 2;
|
|
73
|
+
optional Exemplar exemplar = 3;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
message BucketSpan {
|
|
77
|
+
optional sint32 offset = 1;
|
|
78
|
+
optional uint32 length = 2;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
message Exemplar {
|
|
82
|
+
repeated LabelPair label = 1;
|
|
83
|
+
optional double value = 2;
|
|
84
|
+
optional .google.protobuf.Timestamp timestamp = 3;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
message Metric {
|
|
88
|
+
repeated LabelPair label = 1;
|
|
89
|
+
optional Gauge gauge = 2;
|
|
90
|
+
optional Counter counter = 3;
|
|
91
|
+
optional Summary summary = 4;
|
|
92
|
+
optional Untyped untyped = 5;
|
|
93
|
+
optional Histogram histogram = 7;
|
|
94
|
+
optional int64 timestamp_ms = 6;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
message MetricFamily {
|
|
98
|
+
optional string name = 1;
|
|
99
|
+
optional string help = 2;
|
|
100
|
+
optional MetricType type = 3;
|
|
101
|
+
repeated Metric metric = 4;
|
|
102
|
+
optional string unit = 5;
|
|
103
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { JsonObject } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export function assertCanInfer(status: JsonObject): void {
|
|
4
|
+
if (status.canUseCli !== true) throw new Error("Could not confirm that this account may use the CLI.");
|
|
5
|
+
if (status.dailyQuotaRemainingPercent === 0 || status.weeklyQuotaRemainingPercent === 0) throw new Error("The reported quota is 0, so no inference is run.");
|
|
6
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { awaitWithSignal, ConnectClient, endpoint, RPCError, safeError } from "../protocol/transport.ts";
|
|
4
|
+
import { requiredString, type Credential } from "../types.ts";
|
|
5
|
+
import { exchangeBrowserCode } from "./rest-exchange.ts";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_API_SERVER_URL = "https://server.codeium.com";
|
|
8
|
+
export const DEFAULT_WEBAPP_URL = "https://app.devin.ai";
|
|
9
|
+
export const DEFAULT_DEVIN_API_URL = "https://api.devin.ai";
|
|
10
|
+
const EXCHANGE_METHOD = "exa.seat_management_pb.SeatManagementService/ExchangeDevinCLIPKCECode";
|
|
11
|
+
const PKCE_EXCHANGE_METHOD = "exa.seat_management_pb.SeatManagementService/ExchangePKCEAuthorizationCode";
|
|
12
|
+
const PKCE_CODE_PREFIX = "wspkce$";
|
|
13
|
+
const codeForExchange = (code: string) => code.startsWith(PKCE_CODE_PREFIX) ? code.slice(PKCE_CODE_PREFIX.length) : code;
|
|
14
|
+
export type LoginFlow = "browser" | "manual" | "legacy-connect";
|
|
15
|
+
export interface LoginOptions {
|
|
16
|
+
apiServerURL?: string;
|
|
17
|
+
webappURL?: string;
|
|
18
|
+
devinAPIURL?: string;
|
|
19
|
+
timeoutMs?: number;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
onURL: (url: string, signal: AbortSignal) => void | Promise<void>;
|
|
22
|
+
/** Receives a one-time authorization code, never an API key or session token. */
|
|
23
|
+
promptCode?: (signal: AbortSignal) => Promise<string>;
|
|
24
|
+
fetch?: typeof fetch;
|
|
25
|
+
encoding?: "proto" | "json";
|
|
26
|
+
flow?: LoginFlow;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createPKCE(): { verifier: string; challenge: string; state: string } {
|
|
30
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
31
|
+
return {
|
|
32
|
+
verifier,
|
|
33
|
+
challenge: createHash("sha256").update(verifier).digest("base64url"),
|
|
34
|
+
state: randomBytes(32).toString("base64url"),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function matchesState(received: string | null, expected: string): boolean {
|
|
39
|
+
if (!received) return false;
|
|
40
|
+
const left = Buffer.from(received), right = Buffer.from(expected);
|
|
41
|
+
return left.length === right.length && timingSafeEqual(left, right);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isAuthorizationCode(code: string): boolean {
|
|
45
|
+
return code.length > 0 && code.length <= 8192 && codeForExchange(code).length > 0
|
|
46
|
+
&& !/[\s\u0000-\u001f\u007f]/.test(code)
|
|
47
|
+
&& !/^(devin-session-token\$|cog_|sk-ws-01-|Bearer |Basic )/.test(code);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The new account UI defaults to the REST browser flow. An explicit encoding keeps the legacy Connect path. */
|
|
51
|
+
export function loginAccount(options: LoginOptions): Promise<Credential> {
|
|
52
|
+
return login({ ...options, flow: options.flow ?? (options.encoding === undefined ? "browser" : "legacy-connect") });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Keeps the default Connect contract of the existing shared login(). Storage and opening the browser belong to the caller. */
|
|
56
|
+
export async function login(options: LoginOptions): Promise<Credential> {
|
|
57
|
+
const flow = options.flow ?? "legacy-connect";
|
|
58
|
+
if (!["browser", "manual", "legacy-connect"].includes(flow)) throw new Error("Unsupported login flow.");
|
|
59
|
+
if (options.encoding !== undefined && options.encoding !== "proto" && options.encoding !== "json") throw new Error("The login encoding must be proto or json.");
|
|
60
|
+
if (flow === "browser" && options.encoding !== undefined) throw new Error("A Connect encoding cannot be used with the REST browser login.");
|
|
61
|
+
if (flow !== "browser" && options.devinAPIURL !== undefined) throw new Error("The Devin REST login server can only be set for the REST browser flow.");
|
|
62
|
+
if (flow === "manual" && !options.promptCode) throw new Error("Manual login requires an authorization code prompt.");
|
|
63
|
+
const apiServerURL = endpoint(options.apiServerURL ?? DEFAULT_API_SERVER_URL);
|
|
64
|
+
const webappURL = endpoint(options.webappURL ?? DEFAULT_WEBAPP_URL);
|
|
65
|
+
const devinAPIURL = endpoint(options.devinAPIURL ?? DEFAULT_DEVIN_API_URL);
|
|
66
|
+
const signal = AbortSignal.any([...(options.signal ? [options.signal] : []), AbortSignal.timeout(options.timeoutMs ?? 300_000)]);
|
|
67
|
+
signal.throwIfAborted();
|
|
68
|
+
const pkce = createPKCE();
|
|
69
|
+
if (flow !== "legacy-connect") pkce.state = randomUUID();
|
|
70
|
+
let resolveCode!: (code: string) => void, rejectCode!: (error: Error) => void;
|
|
71
|
+
let hasAcceptedCode = false, authorizationCode = "";
|
|
72
|
+
let client: ConnectClient | undefined;
|
|
73
|
+
let redirectURL: string | undefined;
|
|
74
|
+
const inputController = new AbortController();
|
|
75
|
+
const inputSignal = AbortSignal.any([signal, inputController.signal]);
|
|
76
|
+
const codePromise = new Promise<string>((resolve, reject) => { resolveCode = resolve; rejectCode = reject; });
|
|
77
|
+
void codePromise.catch(() => {});
|
|
78
|
+
const acceptCode = (code: string) => {
|
|
79
|
+
if (hasAcceptedCode || !isAuthorizationCode(code)) return false;
|
|
80
|
+
hasAcceptedCode = true;
|
|
81
|
+
authorizationCode = code;
|
|
82
|
+
resolveCode(code);
|
|
83
|
+
inputController.abort(new DOMException("Authorization code received.", "AbortError"));
|
|
84
|
+
return true;
|
|
85
|
+
};
|
|
86
|
+
const server = createServer((request, response) => {
|
|
87
|
+
response.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
88
|
+
response.setHeader("Cache-Control", "no-store");
|
|
89
|
+
response.setHeader("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'");
|
|
90
|
+
response.setHeader("Referrer-Policy", "no-referrer");
|
|
91
|
+
let url: URL;
|
|
92
|
+
try { url = new URL(request.url ?? "/", "http://127.0.0.1"); }
|
|
93
|
+
catch { response.writeHead(400).end("Invalid callback URL"); return; }
|
|
94
|
+
if (request.method !== "GET" || url.pathname !== "/callback") { response.writeHead(404).end("Not found"); return; }
|
|
95
|
+
if (hasAcceptedCode || !matchesState(url.searchParams.get("state"), pkce.state)) { response.writeHead(400).end("Invalid OAuth state"); return; }
|
|
96
|
+
if (url.searchParams.has("error")) {
|
|
97
|
+
hasAcceptedCode = true;
|
|
98
|
+
response.writeHead(400).end("Login was denied.");
|
|
99
|
+
rejectCode(new Error("OAuth login was denied."));
|
|
100
|
+
inputController.abort();
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (!acceptCode(url.searchParams.get("code") ?? "")) { response.writeHead(400).end("Missing authorization code"); return; }
|
|
104
|
+
response.end("Authorization code received. Check the token exchange result in your terminal.");
|
|
105
|
+
});
|
|
106
|
+
const onAbort = () => rejectCode(new DOMException(
|
|
107
|
+
signal.reason?.name === "TimeoutError" ? "OAuth login timed out." : "OAuth login was cancelled.",
|
|
108
|
+
signal.reason?.name === "TimeoutError" ? "TimeoutError" : "AbortError",
|
|
109
|
+
));
|
|
110
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
111
|
+
try {
|
|
112
|
+
if (flow !== "manual") {
|
|
113
|
+
await new Promise<void>((resolve, reject) => {
|
|
114
|
+
server.once("error", reject);
|
|
115
|
+
server.listen(0, "127.0.0.1", () => { server.off("error", reject); resolve(); });
|
|
116
|
+
});
|
|
117
|
+
const address = server.address();
|
|
118
|
+
if (!address || typeof address === "string") throw new Error("Could not determine the callback port.");
|
|
119
|
+
redirectURL = `http://127.0.0.1:${address.port}/callback`;
|
|
120
|
+
}
|
|
121
|
+
const authorizationURL = new URL(flow === "manual" ? "/devin/account/login" : "/auth/cli/continue", webappURL);
|
|
122
|
+
authorizationURL.search = new URLSearchParams({
|
|
123
|
+
redirect_uri: flow === "manual" ? "chisel-show-auth-token" : redirectURL!,
|
|
124
|
+
state: pkce.state,
|
|
125
|
+
...(flow === "manual" ? {} : { prompt: "select_account" }),
|
|
126
|
+
code_challenge: pkce.challenge, code_challenge_method: "S256",
|
|
127
|
+
}).toString();
|
|
128
|
+
signal.throwIfAborted();
|
|
129
|
+
await awaitWithSignal(Promise.resolve(options.onURL(authorizationURL.href, signal)), signal);
|
|
130
|
+
if (options.promptCode && !hasAcceptedCode) {
|
|
131
|
+
void awaitWithSignal(Promise.resolve().then(() => options.promptCode!(inputSignal)), inputSignal).then((value) => {
|
|
132
|
+
if (hasAcceptedCode || signal.aborted) return;
|
|
133
|
+
authorizationCode = value.trim(); // Also keeps the raw code out of errors.
|
|
134
|
+
let code = authorizationCode;
|
|
135
|
+
if (/^https?:\/\//.test(code)) {
|
|
136
|
+
const pasted = new URL(code);
|
|
137
|
+
if (!redirectURL || pasted.origin + pasted.pathname !== redirectURL || !matchesState(pasted.searchParams.get("state"), pkce.state)) {
|
|
138
|
+
throw new Error("The pasted callback URL or state does not match.");
|
|
139
|
+
}
|
|
140
|
+
code = pasted.searchParams.get("code") ?? "";
|
|
141
|
+
}
|
|
142
|
+
if (!acceptCode(code)) throw new Error("Enter a valid authorization code. API keys and session tokens are not accepted.");
|
|
143
|
+
}).catch((error) => { if (!hasAcceptedCode && !signal.aborted) rejectCode(error); });
|
|
144
|
+
}
|
|
145
|
+
authorizationCode = await codePromise;
|
|
146
|
+
signal.throwIfAborted();
|
|
147
|
+
let sessionToken: string;
|
|
148
|
+
if (flow === "browser") {
|
|
149
|
+
sessionToken = await exchangeBrowserCode({ origin: devinAPIURL, code: authorizationCode, verifier: pkce.verifier, signal, fetch: options.fetch });
|
|
150
|
+
} else {
|
|
151
|
+
client = new ConnectClient({ baseURL: apiServerURL, timeoutMs: 30_000, fetch: options.fetch });
|
|
152
|
+
const isWindsurfCode = authorizationCode.startsWith(PKCE_CODE_PREFIX);
|
|
153
|
+
const method = isWindsurfCode ? PKCE_EXCHANGE_METHOD : EXCHANGE_METHOD;
|
|
154
|
+
const body = isWindsurfCode ? { authorizationCode: codeForExchange(authorizationCode), codeVerifier: pkce.verifier }
|
|
155
|
+
: { code: authorizationCode, codeVerifier: pkce.verifier };
|
|
156
|
+
const exchanged = await client.unary(method, body, signal, { encoding: options.encoding ?? "proto" });
|
|
157
|
+
if (isWindsurfCode && typeof exchanged.apiServerUrl === "string" && exchanged.apiServerUrl && endpoint(exchanged.apiServerUrl) !== apiServerURL) {
|
|
158
|
+
throw new Error("The login returned a different API server. Set the target with --api-server-url and sign in again.");
|
|
159
|
+
}
|
|
160
|
+
const tokenField = isWindsurfCode ? "apiKey" : "sessionToken";
|
|
161
|
+
sessionToken = requiredString(exchanged[tokenField], `${method}.${tokenField}`);
|
|
162
|
+
}
|
|
163
|
+
return { version: 1, sessionToken, apiServerURL, createdAt: new Date().toISOString() };
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const secrets = [authorizationCode, codeForExchange(authorizationCode), pkce.verifier];
|
|
166
|
+
if (error instanceof RPCError) { error.message = safeError(error, secrets); error.code = safeError(error.code, secrets); throw error; }
|
|
167
|
+
const failure = new Error(safeError(error, secrets));
|
|
168
|
+
if (error instanceof Error) failure.name = error.name;
|
|
169
|
+
throw failure;
|
|
170
|
+
} finally {
|
|
171
|
+
inputController.abort();
|
|
172
|
+
client?.close();
|
|
173
|
+
signal.removeEventListener("abort", onAbort);
|
|
174
|
+
server.closeAllConnections();
|
|
175
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createHTTPTransport } from "../protocol/http.ts";
|
|
2
|
+
import { awaitWithSignal, boundedJSON, discardResponse, endpoint, RPCError } from "../protocol/transport.ts";
|
|
3
|
+
import { object, requiredString } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
const TOKEN_PREFIX = "devin-session-token$";
|
|
6
|
+
|
|
7
|
+
/** REST authorization code exchange, same as the public client. Never resends or creates a refresh grant. */
|
|
8
|
+
export async function exchangeBrowserCode(options: {
|
|
9
|
+
origin: string; code: string; verifier: string; signal: AbortSignal; fetch?: typeof fetch;
|
|
10
|
+
}): Promise<string> {
|
|
11
|
+
const origin = endpoint(options.origin);
|
|
12
|
+
const signal = AbortSignal.any([options.signal, AbortSignal.timeout(30_000)]);
|
|
13
|
+
signal.throwIfAborted();
|
|
14
|
+
const transport = options.fetch ? undefined : createHTTPTransport();
|
|
15
|
+
try {
|
|
16
|
+
const response = await awaitWithSignal((options.fetch ?? transport!.fetch)(`${origin}/auth/cli/token`, {
|
|
17
|
+
method: "POST", redirect: "error", signal,
|
|
18
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
19
|
+
body: JSON.stringify({ code: options.code, code_verifier: options.verifier }),
|
|
20
|
+
}), signal, discardResponse);
|
|
21
|
+
if (!response.ok) {
|
|
22
|
+
discardResponse(response);
|
|
23
|
+
throw new RPCError(response.status === 401 ? "unauthenticated" : `http_${response.status}`, "Devin login code exchange failed", response.status);
|
|
24
|
+
}
|
|
25
|
+
let body: unknown;
|
|
26
|
+
try { body = await boundedJSON(response, 64 * 1024, signal); }
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof SyntaxError) throw new Error("The Devin login response is not valid JSON.");
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
const token = requiredString(object(body, "Devin login response").token, "Devin login response.token");
|
|
32
|
+
if (/[\s\u0000-\u001f\u007f]/.test(token)) throw new Error("The token in the Devin login response has an invalid format.");
|
|
33
|
+
return token.startsWith(TOKEN_PREFIX) ? token : TOKEN_PREFIX + token;
|
|
34
|
+
} finally { transport?.close(); }
|
|
35
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { parseModelCatalog, type ModelCatalog } from "./models/catalog.ts";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { ConnectClient, ASSIGN_MODEL_METHOD, endpoint, type ConnectClientOptions } from "./protocol/transport.ts";
|
|
4
|
+
import { createFingerprintProvider, type FingerprintProvider } from "./protocol/fingerprint.ts";
|
|
5
|
+
import { wireMessages } from "./inference/messages.ts";
|
|
6
|
+
import { object, requiredString, type ChatMessage, type Credential, type JsonObject, type ModelInfo } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
export const CLIENT_VERSION = "3000.11.92";
|
|
9
|
+
export type MetadataProfile = "agent" | "standalone" | "login";
|
|
10
|
+
const MODEL_DISPLAYS = [
|
|
11
|
+
"DISPLAY_OPTION_MODEL_ROUTER", "DISPLAY_OPTION_QUICK_REVIEW", "DISPLAY_OPTION_HIDDEN_TOOL",
|
|
12
|
+
"DISPLAY_OPTION_WIRE_7", "DISPLAY_OPTION_MODERN_AGENT_CHAT",
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
export interface DevinClientOptions extends Omit<ConnectClientOptions, "baseURL" | "token"> {
|
|
16
|
+
profile?: MetadataProfile;
|
|
17
|
+
restBaseURL?: string;
|
|
18
|
+
fingerprint?: FingerprintProvider;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class DevinClient {
|
|
22
|
+
readonly credential: Credential;
|
|
23
|
+
readonly transport: ConnectClient;
|
|
24
|
+
readonly profile: MetadataProfile;
|
|
25
|
+
readonly fingerprint: FingerprintProvider;
|
|
26
|
+
defaultModelUid?: string;
|
|
27
|
+
accountIdentityHash?: string;
|
|
28
|
+
private organizationId?: string;
|
|
29
|
+
private restBaseURL: string;
|
|
30
|
+
private readonly hasExplicitRestURL: boolean;
|
|
31
|
+
constructor(credential: Credential, options: DevinClientOptions = {}) {
|
|
32
|
+
this.credential = credential;
|
|
33
|
+
const { profile, fingerprint, restBaseURL, ...transportOptions } = options;
|
|
34
|
+
const rpcOrigin = endpoint(credential.apiServerURL);
|
|
35
|
+
const isLoopback = ["localhost", "127.0.0.1", "[::1]"].includes(new URL(rpcOrigin).hostname);
|
|
36
|
+
this.hasExplicitRestURL = restBaseURL !== undefined;
|
|
37
|
+
this.restBaseURL = endpoint(restBaseURL ?? (isLoopback ? rpcOrigin : "https://api.devin.ai"));
|
|
38
|
+
this.profile = profile ?? "agent";
|
|
39
|
+
this.fingerprint = fingerprint ?? createFingerprintProvider(credential.sessionToken);
|
|
40
|
+
this.transport = new ConnectClient({ baseURL: credential.apiServerURL, token: credential.sessionToken, ...transportOptions });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
metadata(options: { profile?: MetadataProfile; includeModelDisplays?: boolean } = {}): JsonObject {
|
|
44
|
+
const profile = options.profile ?? this.profile;
|
|
45
|
+
const version = profile === "standalone" ? "0.0.0-dev" : CLIENT_VERSION;
|
|
46
|
+
return {
|
|
47
|
+
apiKey: this.credential.sessionToken,
|
|
48
|
+
ideName: profile === "agent" ? "devin-cli" : "chisel",
|
|
49
|
+
ideVersion: version,
|
|
50
|
+
extensionName: "chisel",
|
|
51
|
+
extensionVersion: version,
|
|
52
|
+
locale: "en",
|
|
53
|
+
os: process.platform,
|
|
54
|
+
...(profile === "agent" ? { ideType: "chisel" } : {}),
|
|
55
|
+
...(options.includeModelDisplays ? { supportedModelDisplays: [...MODEL_DISPLAYS] } : {}),
|
|
56
|
+
f: this.fingerprint.create(),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async status(signal?: AbortSignal, expectedUserId?: string, profile = this.profile): Promise<JsonObject> {
|
|
61
|
+
signal?.throwIfAborted();
|
|
62
|
+
const response = await this.transport.unary(
|
|
63
|
+
"exa.seat_management_pb.SeatManagementService/GetUserStatus", { metadata: this.metadata({ profile }) }, signal,
|
|
64
|
+
);
|
|
65
|
+
const user = object(response.userStatus, "userStatus");
|
|
66
|
+
if (expectedUserId && user.userId !== expectedUserId) {
|
|
67
|
+
throw new Error("The signed-in user ID differs from the requested account. Sign in again; no inference was run.");
|
|
68
|
+
}
|
|
69
|
+
const plan = object(user.planStatus ?? {}, "planStatus");
|
|
70
|
+
const info = object(plan.planInfo ?? {}, "planInfo");
|
|
71
|
+
const devin = object(info.devinInfo ?? {}, "devinInfo");
|
|
72
|
+
const userId = requiredString(user.userId, "userStatus.userId");
|
|
73
|
+
this.accountIdentityHash = createHash("sha256").update(`${this.transport.baseURL}\0${userId}`).digest("hex");
|
|
74
|
+
this.organizationId = typeof devin.orgId === "string" ? devin.orgId : undefined;
|
|
75
|
+
if (!this.hasExplicitRestURL && typeof devin.apiUrl === "string" && devin.apiUrl) {
|
|
76
|
+
const candidate = endpoint(devin.apiUrl);
|
|
77
|
+
if (candidate !== this.restBaseURL && candidate !== this.transport.baseURL) throw new Error("Unverified REST server. Specify it explicitly with --rest-server.");
|
|
78
|
+
this.restBaseURL = candidate;
|
|
79
|
+
}
|
|
80
|
+
const hasQuota = info.billingStrategy === "BILLING_STRATEGY_QUOTA";
|
|
81
|
+
// Name, email, organization ID and the raw response are not exposed.
|
|
82
|
+
return {
|
|
83
|
+
...(expectedUserId ? { identityVerified: true } : {}),
|
|
84
|
+
teamsTier: user.teamsTier,
|
|
85
|
+
canUseCli: info.devinInfo ? devin.canUseCli === true : undefined,
|
|
86
|
+
billingStrategy: info.billingStrategy,
|
|
87
|
+
dailyQuotaRemainingPercent: plan.dailyQuotaRemainingPercent ?? (hasQuota ? 0 : undefined),
|
|
88
|
+
weeklyQuotaRemainingPercent: plan.weeklyQuotaRemainingPercent ?? (hasQuota ? 0 : undefined),
|
|
89
|
+
dailyQuotaResetAtUnix: plan.dailyQuotaResetAtUnix,
|
|
90
|
+
weeklyQuotaResetAtUnix: plan.weeklyQuotaResetAtUnix,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async catalog(signal?: AbortSignal): Promise<ModelCatalog> {
|
|
95
|
+
signal?.throwIfAborted();
|
|
96
|
+
const response = await this.transport.unary(
|
|
97
|
+
"exa.api_server_pb.ApiServerService/GetCliModelConfigs", { metadata: this.metadata({ includeModelDisplays: true }) }, signal,
|
|
98
|
+
);
|
|
99
|
+
const catalog = parseModelCatalog(response);
|
|
100
|
+
this.defaultModelUid = catalog.defaultModelUid;
|
|
101
|
+
return catalog;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async models(signal?: AbortSignal): Promise<ModelInfo[]> {
|
|
105
|
+
return (await this.catalog(signal)).models;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async teamSettings(signal?: AbortSignal): Promise<JsonObject> {
|
|
109
|
+
signal?.throwIfAborted();
|
|
110
|
+
return this.transport.unary("exa.seat_management_pb.SeatManagementService/GetCliTeamSettings",
|
|
111
|
+
{ metadata: this.metadata({ includeModelDisplays: true }) }, signal);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async managedPlugins(signal?: AbortSignal): Promise<JsonObject> {
|
|
115
|
+
signal?.throwIfAborted();
|
|
116
|
+
// Only fetches the manifest. This does not mean installing a remote plugin or running its commands is approved.
|
|
117
|
+
return this.transport.unary("exa.api_server_pb.ApiServerService/GetAccountManagedPlugins", { metadata: this.metadata() }, signal);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async assignModel(routerUid: string, cascadeId: string, message: ChatMessage | undefined, signal?: AbortSignal): Promise<{ modelUid: string; assignmentJwt: string }> {
|
|
121
|
+
signal?.throwIfAborted();
|
|
122
|
+
const prompt = message ? wireMessages([message])[0] : undefined;
|
|
123
|
+
if (prompt) delete prompt.promptCacheOptions;
|
|
124
|
+
const response = await this.transport.unary(ASSIGN_MODEL_METHOD, {
|
|
125
|
+
metadata: this.metadata(), modelRouterUid: routerUid, cascadeId, ...(prompt ? { chatMessagePrompt: prompt } : {}),
|
|
126
|
+
}, signal);
|
|
127
|
+
const assignment = object(response.assignment, "assignment");
|
|
128
|
+
return {
|
|
129
|
+
modelUid: requiredString(assignment.modelUid, "assignment.modelUid"),
|
|
130
|
+
assignmentJwt: message ? requiredString(assignment.assignmentJwt, "assignment.assignmentJwt") : String(assignment.assignmentJwt ?? ""),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async self(signal?: AbortSignal): Promise<JsonObject> {
|
|
135
|
+
return this.transport.getJSON(this.restBaseURL, "/v3/self", signal);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async integrations(signal?: AbortSignal): Promise<JsonObject | undefined> {
|
|
139
|
+
if (!this.organizationId) return undefined;
|
|
140
|
+
return this.transport.getJSON(this.restBaseURL, `/v3/organizations/${encodeURIComponent(this.organizationId)}/integrations`, signal);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async analytics(events: JsonObject[], signal?: AbortSignal): Promise<void> {
|
|
144
|
+
signal?.throwIfAborted();
|
|
145
|
+
if (events.length === 0) return;
|
|
146
|
+
await this.transport.unary("exa.product_analytics_pb.ProductAnalyticsService/BatchRecordAnalyticsEvents", {
|
|
147
|
+
events, apiKey: this.credential.sessionToken,
|
|
148
|
+
}, signal);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
export { getModelByUID, selectModel } from "./models/select.ts";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Host-independent API shared by the CLI and Pi. */
|
|
2
|
+
export const CORE_CONTRACT_VERSION = 1;
|
|
3
|
+
export * from "./types.ts";
|
|
4
|
+
export * from "./client.ts";
|
|
5
|
+
export * from "./auth/index.ts";
|
|
6
|
+
export * from "./models/catalog.ts";
|
|
7
|
+
export * from "./inference/chat.ts";
|
|
8
|
+
export * from "./inference/messages.ts";
|
|
9
|
+
export * from "./inference/retry.ts";
|
|
10
|
+
export * from "./inference/state.ts";
|
|
11
|
+
export * from "./protocol/transport.ts";
|
|
12
|
+
export * from "./protocol/fingerprint.ts";
|