@buyong/pi-devin 0.1.5 → 0.1.6
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.ko.md +3 -5
- package/README.md +3 -5
- package/index.ts +6 -15
- package/package.json +3 -1
- package/src/core/auth/access.ts +7 -3
- package/src/core/auth/callback-server.ts +57 -0
- package/src/core/auth/index.ts +2 -1
- package/src/core/auth/login.ts +104 -151
- package/src/core/auth/pkce.ts +32 -0
- package/src/core/auth/rest-exchange.ts +32 -30
- package/src/core/cancellation.ts +99 -0
- package/src/core/client.ts +127 -122
- package/src/core/errors.ts +115 -0
- package/src/core/index.ts +18 -4
- package/src/core/inference/accumulator.ts +174 -0
- package/src/core/inference/images.ts +22 -15
- package/src/core/inference/request.ts +59 -0
- package/src/core/inference/retry.ts +31 -31
- package/src/core/inference/state.ts +7 -3
- package/src/core/inference/stream-chat.ts +48 -0
- package/src/core/models/access.ts +4 -3
- package/src/core/models/catalog.ts +29 -20
- package/src/core/models/select.ts +14 -12
- package/src/core/models/variants.ts +85 -72
- package/src/core/protocol/connect-client.ts +288 -0
- package/src/core/protocol/device.ts +33 -34
- package/src/core/protocol/endpoint.ts +22 -0
- package/src/core/protocol/fingerprint.ts +38 -26
- package/src/core/protocol/frames.ts +101 -0
- package/src/core/protocol/http/fetch-client.ts +42 -0
- package/src/core/protocol/http/node-client.ts +130 -0
- package/src/core/protocol/http/response.ts +99 -0
- package/src/core/protocol/http.ts +41 -101
- package/src/core/protocol/protobuf.ts +93 -74
- package/src/core/redaction.ts +33 -0
- package/src/core/services/device-identity.ts +15 -0
- package/src/core/services/http-transport.ts +28 -0
- package/src/core/services/platform.ts +54 -0
- package/src/core/types.ts +2 -23
- package/src/core/validation.ts +25 -0
- package/src/pi/adapters/pi/devin-command.ts +82 -0
- package/src/pi/adapters/pi/fusion-command.ts +92 -0
- package/src/pi/adapters/pi/oauth.ts +14 -0
- package/src/pi/adapters/pi/provider.ts +22 -0
- package/src/pi/adapters/pi/registration.ts +89 -0
- package/src/pi/adapters/pi/response-stream.ts +164 -0
- package/src/pi/adapters/pi/sidekick-tool.ts +45 -0
- package/src/pi/adapters/pi/transcript.ts +177 -0
- package/src/pi/adapters/pi/types.ts +14 -0
- package/src/pi/{devin-panel.ts → adapters/pi/ui/devin-panel.ts} +11 -9
- package/src/pi/{fusion-picker.ts → adapters/pi/ui/fusion-picker.ts} +1 -1
- package/src/pi/adapters/pi/usage-status.ts +65 -0
- package/src/pi/adapters/storage/files.ts +66 -0
- package/src/pi/adapters/storage/json-file.ts +31 -0
- package/src/pi/app/config.ts +25 -0
- package/src/pi/app/environment.ts +30 -0
- package/src/pi/app/layer.ts +43 -0
- package/src/pi/app/runtime.ts +20 -0
- package/src/pi/app/session.ts +26 -0
- package/src/pi/features/accounts/service.ts +69 -0
- package/src/pi/features/accounts/types.ts +25 -0
- package/src/pi/features/auth/credential.ts +40 -0
- package/src/pi/features/auth/guard.ts +25 -0
- package/src/pi/features/auth/login.ts +116 -0
- package/src/pi/features/auth/state.ts +48 -0
- package/src/pi/features/auth/types.ts +36 -0
- package/src/pi/features/bounded.ts +4 -0
- package/src/pi/features/catalog/constants.ts +10 -0
- package/src/pi/features/catalog/fusion.ts +45 -0
- package/src/pi/features/catalog/ids.ts +79 -0
- package/src/pi/features/catalog/projection.ts +93 -0
- package/src/pi/features/catalog/remote.ts +15 -0
- package/src/pi/features/catalog/restore.ts +53 -0
- package/src/pi/features/catalog/service.ts +158 -0
- package/src/pi/features/catalog/types.ts +56 -0
- package/src/pi/features/clients/service.ts +47 -0
- package/src/pi/features/clients/types.ts +22 -0
- package/src/pi/features/default-scheduler.ts +12 -0
- package/src/pi/features/inference/request.ts +190 -0
- package/src/pi/features/inference/service.ts +33 -0
- package/src/pi/features/inference/types.ts +11 -0
- package/src/pi/features/lifetime/service.ts +19 -0
- package/src/pi/features/lifetime/types.ts +13 -0
- package/src/pi/features/runs/admission.ts +36 -0
- package/src/pi/features/runs/lifecycle.ts +207 -0
- package/src/pi/features/runs/service.ts +10 -0
- package/src/pi/features/runs/state.ts +37 -0
- package/src/pi/features/runs/types.ts +43 -0
- package/src/pi/features/settings/service.ts +46 -0
- package/src/pi/features/settings/types.ts +36 -0
- package/src/pi/features/sidekick/child.ts +228 -0
- package/src/pi/features/sidekick/command.ts +80 -0
- package/src/pi/features/sidekick/launcher.ts +12 -0
- package/src/pi/features/sidekick/prompts.ts +17 -0
- package/src/pi/features/sidekick/report.ts +51 -0
- package/src/pi/features/sidekick/service.ts +176 -0
- package/src/pi/features/sidekick/types.ts +99 -0
- package/src/pi/features/usage/changes.ts +20 -0
- package/src/pi/{usage.ts → features/usage/report.ts} +3 -34
- package/src/pi/features/usage/types.ts +33 -0
- package/src/core/inference/chat.ts +0 -234
- package/src/core/protocol/transport.ts +0 -513
- package/src/pi/auth.ts +0 -166
- package/src/pi/devin-command.ts +0 -86
- package/src/pi/devin-settings.ts +0 -50
- package/src/pi/extension.ts +0 -89
- package/src/pi/fusion-command.ts +0 -83
- package/src/pi/fusion-settings.ts +0 -44
- package/src/pi/lifecycle.ts +0 -217
- package/src/pi/messages.ts +0 -131
- package/src/pi/models.ts +0 -415
- package/src/pi/provider.ts +0 -278
- package/src/pi/sidekick-report.ts +0 -47
- package/src/pi/sidekick-runtime.ts +0 -300
- package/src/pi/sidekick.ts +0 -211
- package/src/pi/stream.ts +0 -124
- package/src/pi/usage-store.ts +0 -75
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Deferred, Duration, Effect, Exit, type Scope } from "effect";
|
|
2
|
+
import { RequestTimeout, toDevinFailure, type DevinFailure } from "./errors.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A one-shot stop switch that carries its reason. Interruption has no reason, so lifetimes such as a closed client,
|
|
6
|
+
* an invalidated login or an ended run fail the guarded work with the reason instead.
|
|
7
|
+
*/
|
|
8
|
+
export interface Cancellation {
|
|
9
|
+
/** Fails with the reason once cancelled; never succeeds. */
|
|
10
|
+
readonly await: Effect.Effect<never, DevinFailure>;
|
|
11
|
+
readonly isCancelled: () => boolean;
|
|
12
|
+
readonly reason: () => DevinFailure | undefined;
|
|
13
|
+
/** Cancels once; later reasons are ignored. Also usable from synchronous host callbacks. */
|
|
14
|
+
readonly cancelUnsafe: (reason: DevinFailure) => void;
|
|
15
|
+
readonly cancel: (reason: DevinFailure) => Effect.Effect<void>;
|
|
16
|
+
/** Stops the effect when cancelled and fails it with the reason. Already cancelled work does not start. */
|
|
17
|
+
readonly guard: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E | DevinFailure, R>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function makeCancellation(): Cancellation {
|
|
21
|
+
const deferred = Deferred.makeUnsafe<never, DevinFailure>();
|
|
22
|
+
let current: DevinFailure | undefined;
|
|
23
|
+
const cancelUnsafe = (reason: DevinFailure) => {
|
|
24
|
+
if (current) return;
|
|
25
|
+
current = reason;
|
|
26
|
+
Deferred.doneUnsafe(deferred, Exit.fail(reason));
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
await: Deferred.await(deferred),
|
|
30
|
+
isCancelled: () => current !== undefined,
|
|
31
|
+
reason: () => current,
|
|
32
|
+
cancelUnsafe,
|
|
33
|
+
cancel: (reason) => Effect.sync(() => cancelUnsafe(reason)),
|
|
34
|
+
guard: (effect) => Effect.suspend(() => current ? Effect.fail(current) : Effect.raceFirst(effect, Deferred.await(deferred))),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Guards an effect with several cancellations; the first one wins. */
|
|
39
|
+
export function guardAll(cancellations: readonly Cancellation[]) {
|
|
40
|
+
return <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E | DevinFailure, R> =>
|
|
41
|
+
cancellations.reduce<Effect.Effect<A, E | DevinFailure, R>>((guarded, cancellation) => cancellation.guard(guarded), effect);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Starts a deadline at once. The timer is registered before the guarded work runs any of its own code,
|
|
46
|
+
* so hooks that advance or block time cannot slip past it. The timer stops with the scope.
|
|
47
|
+
*/
|
|
48
|
+
export function startDeadline(timeoutMs: number, reason: () => DevinFailure = () => new RequestTimeout({ message: `The request exceeded the ${timeoutMs}ms timeout.` })): Effect.Effect<Cancellation, never, Scope.Scope> {
|
|
49
|
+
return Effect.gen(function* () {
|
|
50
|
+
const deadline = makeCancellation();
|
|
51
|
+
yield* Effect.forkScoped(Effect.sleep(Duration.millis(timeoutMs)).pipe(Effect.andThen(deadline.cancel(reason()))), { startImmediately: true });
|
|
52
|
+
return deadline;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** A deadline that restarts whenever data arrives. */
|
|
57
|
+
export interface IdleDeadline extends Cancellation {
|
|
58
|
+
/** Restarts the wait. Has no effect once the deadline has passed. */
|
|
59
|
+
readonly touch: Effect.Effect<void>;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Starts an idle deadline at once: it passes only when nothing arrives for `timeoutMs`, so a long response that keeps
|
|
64
|
+
* streaming is not cut off. Like `startDeadline`, the timer exists before the guarded work runs. It stops with the scope.
|
|
65
|
+
*/
|
|
66
|
+
export function startIdleDeadline(timeoutMs: number, reason: () => DevinFailure = () => new RequestTimeout({ message: `The request received no data for ${timeoutMs}ms.` })): Effect.Effect<IdleDeadline, never, Scope.Scope> {
|
|
67
|
+
return Effect.gen(function* () {
|
|
68
|
+
const deadline = makeCancellation();
|
|
69
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
70
|
+
const arm = () => {
|
|
71
|
+
clearTimeout(timer);
|
|
72
|
+
if (!deadline.isCancelled()) timer = setTimeout(() => deadline.cancelUnsafe(reason()), timeoutMs);
|
|
73
|
+
};
|
|
74
|
+
arm();
|
|
75
|
+
yield* Effect.addFinalizer(() => Effect.sync(() => clearTimeout(timer)));
|
|
76
|
+
return { ...deadline, touch: Effect.sync(arm) };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Fails with the signal's reason once it aborts; never succeeds. */
|
|
81
|
+
export function whenAborted(signal: AbortSignal): Effect.Effect<never, DevinFailure> {
|
|
82
|
+
return Effect.callback<never, DevinFailure>((resume) => {
|
|
83
|
+
const onAbort = () => resume(Effect.fail(toDevinFailure(signal.reason)));
|
|
84
|
+
if (signal.aborted) { onAbort(); return; }
|
|
85
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
86
|
+
return Effect.sync(() => signal.removeEventListener("abort", onAbort));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Bridges a host `AbortSignal`: the effect stops when it aborts and fails with the signal's reason,
|
|
92
|
+
* because hosts tell cancellation from a timeout by that reason.
|
|
93
|
+
*/
|
|
94
|
+
export function abortWith(signal: AbortSignal | undefined) {
|
|
95
|
+
return <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, E | DevinFailure, R> => {
|
|
96
|
+
if (!signal) return effect;
|
|
97
|
+
return Effect.suspend(() => signal.aborted ? Effect.fail(toDevinFailure(signal.reason)) : Effect.raceFirst(effect, whenAborted(signal)));
|
|
98
|
+
};
|
|
99
|
+
}
|
package/src/core/client.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
|
-
import { parseModelCatalog, type ModelCatalog } from "./models/catalog.ts";
|
|
2
1
|
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { Effect, Predicate, type Scope } from "effect";
|
|
3
|
+
import type { DevinError, DevinFailure } from "./errors.ts";
|
|
5
4
|
import { wireMessages } from "./inference/messages.ts";
|
|
6
|
-
import {
|
|
5
|
+
import { parseModelCatalog, type ModelCatalog } from "./models/catalog.ts";
|
|
6
|
+
import { ASSIGN_MODEL_METHOD, makeConnectClient, type ConnectClient, type ConnectClientOptions } from "./protocol/connect-client.ts";
|
|
7
|
+
import { endpoint, isLoopbackHost } from "./protocol/endpoint.ts";
|
|
8
|
+
import { makeFingerprintProvider, type FingerprintProvider } from "./protocol/fingerprint.ts";
|
|
9
|
+
import { DeviceIdentitySource } from "./services/device-identity.ts";
|
|
10
|
+
import type { HttpTransport } from "./services/http-transport.ts";
|
|
11
|
+
import type { ChatMessage, Credential, JsonObject, ModelInfo } from "./types.ts";
|
|
12
|
+
import { failWith, object, requiredString } from "./validation.ts";
|
|
7
13
|
|
|
8
14
|
export const CLIENT_VERSION = "3000.11.92";
|
|
9
15
|
export type MetadataProfile = "agent" | "standalone" | "login";
|
|
@@ -11,143 +17,142 @@ const MODEL_DISPLAYS = [
|
|
|
11
17
|
"DISPLAY_OPTION_MODEL_ROUTER", "DISPLAY_OPTION_QUICK_REVIEW", "DISPLAY_OPTION_HIDDEN_TOOL",
|
|
12
18
|
"DISPLAY_OPTION_WIRE_7", "DISPLAY_OPTION_MODERN_AGENT_CHAT",
|
|
13
19
|
];
|
|
20
|
+
const STATUS_METHOD = "exa.seat_management_pb.SeatManagementService/GetUserStatus";
|
|
21
|
+
const MODELS_METHOD = "exa.api_server_pb.ApiServerService/GetCliModelConfigs";
|
|
14
22
|
|
|
15
23
|
export interface DevinClientOptions extends Omit<ConnectClientOptions, "baseURL" | "token"> {
|
|
16
24
|
profile?: MetadataProfile;
|
|
17
25
|
restBaseURL?: string;
|
|
26
|
+
/** A provider shared with other clients of the same credential. Otherwise this client collects device values once for itself. */
|
|
18
27
|
fingerprint?: FingerprintProvider;
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
export
|
|
30
|
+
export interface StatusOptions {
|
|
31
|
+
/** Rejects a different signed-in user before anything else is sent. */
|
|
32
|
+
expectedUserId?: string;
|
|
33
|
+
profile?: MetadataProfile;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface DevinClient {
|
|
22
37
|
readonly credential: Credential;
|
|
23
38
|
readonly transport: ConnectClient;
|
|
24
39
|
readonly profile: MetadataProfile;
|
|
25
40
|
readonly fingerprint: FingerprintProvider;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
}
|
|
41
|
+
/** Default model of the last fetched catalog. */
|
|
42
|
+
readonly defaultModelUid: Effect.Effect<string | undefined>;
|
|
43
|
+
/** Hash of the endpoint and user ID from the last status check. */
|
|
44
|
+
readonly accountIdentityHash: Effect.Effect<string | undefined>;
|
|
45
|
+
readonly close: Effect.Effect<void>;
|
|
46
|
+
metadata(options?: { profile?: MetadataProfile; includeModelDisplays?: boolean }): Effect.Effect<JsonObject, DevinError>;
|
|
47
|
+
status(options?: StatusOptions): Effect.Effect<JsonObject, DevinFailure>;
|
|
48
|
+
readonly catalog: Effect.Effect<ModelCatalog, DevinFailure>;
|
|
49
|
+
readonly models: Effect.Effect<ModelInfo[], DevinFailure>;
|
|
50
|
+
readonly teamSettings: Effect.Effect<JsonObject, DevinFailure>;
|
|
51
|
+
readonly managedPlugins: Effect.Effect<JsonObject, DevinFailure>;
|
|
52
|
+
assignModel(routerUid: string, cascadeId: string, message: ChatMessage | undefined): Effect.Effect<{ modelUid: string; assignmentJwt: string }, DevinFailure>;
|
|
53
|
+
readonly self: Effect.Effect<JsonObject, DevinFailure>;
|
|
54
|
+
readonly integrations: Effect.Effect<JsonObject | undefined, DevinFailure>;
|
|
55
|
+
analytics(events: readonly JsonObject[]): Effect.Effect<void, DevinFailure>;
|
|
56
|
+
}
|
|
59
57
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
);
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
const
|
|
70
|
-
|
|
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
|
-
}
|
|
58
|
+
/** A client for one credential. It closes with the scope, or earlier through `close`. */
|
|
59
|
+
export function makeDevinClient(credential: Credential, options: DevinClientOptions = {}): Effect.Effect<DevinClient, DevinError, HttpTransport | DeviceIdentitySource | Scope.Scope> {
|
|
60
|
+
return Effect.gen(function* () {
|
|
61
|
+
const { profile = "agent", fingerprint: providedFingerprint, restBaseURL: explicitRestURL, ...transportOptions } = options;
|
|
62
|
+
const rpcOrigin = yield* endpoint(credential.apiServerURL);
|
|
63
|
+
const hasExplicitRestURL = explicitRestURL !== undefined;
|
|
64
|
+
let restBaseURL = yield* endpoint(explicitRestURL ?? (isLoopbackHost(new URL(rpcOrigin).hostname) ? rpcOrigin : "https://api.devin.ai"));
|
|
65
|
+
const device = yield* DeviceIdentitySource;
|
|
66
|
+
const fingerprint = providedFingerprint ?? (yield* makeFingerprintProvider(credential.sessionToken, { collectIdentity: device.collect }));
|
|
67
|
+
const transport = yield* makeConnectClient({ baseURL: credential.apiServerURL, token: credential.sessionToken, ...transportOptions });
|
|
68
|
+
let defaultModelUid: string | undefined, accountIdentityHash: string | undefined, organizationId: string | undefined;
|
|
93
69
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
70
|
+
const metadata: DevinClient["metadata"] = (metadataOptions = {}) => Effect.map(fingerprint.create, (f) => {
|
|
71
|
+
const selected = metadataOptions.profile ?? profile;
|
|
72
|
+
const version = selected === "standalone" ? "0.0.0-dev" : CLIENT_VERSION;
|
|
73
|
+
return {
|
|
74
|
+
apiKey: credential.sessionToken,
|
|
75
|
+
ideName: selected === "agent" ? "devin-cli" : "chisel",
|
|
76
|
+
ideVersion: version,
|
|
77
|
+
extensionName: "chisel",
|
|
78
|
+
extensionVersion: version,
|
|
79
|
+
locale: "en",
|
|
80
|
+
os: process.platform,
|
|
81
|
+
...(selected === "agent" ? { ideType: "chisel" } : {}),
|
|
82
|
+
...(metadataOptions.includeModelDisplays ? { supportedModelDisplays: [...MODEL_DISPLAYS] } : {}),
|
|
83
|
+
f,
|
|
84
|
+
};
|
|
85
|
+
});
|
|
103
86
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
87
|
+
const status: DevinClient["status"] = (statusOptions = {}) => Effect.gen(function* () {
|
|
88
|
+
const response = yield* transport.unary(STATUS_METHOD, { metadata: yield* metadata({ profile: statusOptions.profile }) });
|
|
89
|
+
const user = yield* object(response.userStatus, "userStatus");
|
|
90
|
+
if (statusOptions.expectedUserId && user.userId !== statusOptions.expectedUserId) {
|
|
91
|
+
return yield* failWith("The signed-in user ID differs from the requested account. Sign in again; no inference was run.");
|
|
92
|
+
}
|
|
93
|
+
const plan = yield* object(user.planStatus ?? {}, "planStatus");
|
|
94
|
+
const info = yield* object(plan.planInfo ?? {}, "planInfo");
|
|
95
|
+
const devin = yield* object(info.devinInfo ?? {}, "devinInfo");
|
|
96
|
+
const userId = yield* requiredString(user.userId, "userStatus.userId");
|
|
97
|
+
accountIdentityHash = createHash("sha256").update(`${transport.baseURL}\0${userId}`).digest("hex");
|
|
98
|
+
organizationId = Predicate.isString(devin.orgId) ? devin.orgId : undefined;
|
|
99
|
+
if (!hasExplicitRestURL && Predicate.isString(devin.apiUrl) && devin.apiUrl) {
|
|
100
|
+
const candidate = yield* endpoint(devin.apiUrl);
|
|
101
|
+
if (candidate !== restBaseURL && candidate !== transport.baseURL) return yield* failWith("Unverified REST server. Specify it explicitly with --rest-server.");
|
|
102
|
+
restBaseURL = candidate;
|
|
103
|
+
}
|
|
104
|
+
const hasQuota = info.billingStrategy === "BILLING_STRATEGY_QUOTA";
|
|
105
|
+
// Name, email, organization ID and the raw response are not exposed.
|
|
106
|
+
return {
|
|
107
|
+
...(statusOptions.expectedUserId ? { identityVerified: true } : {}),
|
|
108
|
+
teamsTier: user.teamsTier,
|
|
109
|
+
canUseCli: info.devinInfo ? devin.canUseCli === true : undefined,
|
|
110
|
+
billingStrategy: info.billingStrategy,
|
|
111
|
+
dailyQuotaRemainingPercent: plan.dailyQuotaRemainingPercent ?? (hasQuota ? 0 : undefined),
|
|
112
|
+
weeklyQuotaRemainingPercent: plan.weeklyQuotaRemainingPercent ?? (hasQuota ? 0 : undefined),
|
|
113
|
+
dailyQuotaResetAtUnix: plan.dailyQuotaResetAtUnix,
|
|
114
|
+
weeklyQuotaResetAtUnix: plan.weeklyQuotaResetAtUnix,
|
|
115
|
+
};
|
|
116
|
+
});
|
|
107
117
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
118
|
+
const catalog = Effect.gen(function* () {
|
|
119
|
+
const response = yield* transport.unary(MODELS_METHOD, { metadata: yield* metadata({ includeModelDisplays: true }) });
|
|
120
|
+
const parsed = yield* parseModelCatalog(response);
|
|
121
|
+
defaultModelUid = parsed.defaultModelUid;
|
|
122
|
+
return parsed;
|
|
123
|
+
});
|
|
113
124
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
125
|
+
const assignModel: DevinClient["assignModel"] = (routerUid, cascadeId, message) => Effect.gen(function* () {
|
|
126
|
+
const prompt = message ? wireMessages([message])[0] : undefined;
|
|
127
|
+
if (prompt) delete prompt.promptCacheOptions;
|
|
128
|
+
const response = yield* transport.unary(ASSIGN_MODEL_METHOD, {
|
|
129
|
+
metadata: yield* metadata(), modelRouterUid: routerUid, cascadeId, ...(prompt ? { chatMessagePrompt: prompt } : {}),
|
|
130
|
+
});
|
|
131
|
+
const assignment = yield* object(response.assignment, "assignment");
|
|
132
|
+
return {
|
|
133
|
+
modelUid: yield* requiredString(assignment.modelUid, "assignment.modelUid"),
|
|
134
|
+
assignmentJwt: message ? yield* requiredString(assignment.assignmentJwt, "assignment.assignmentJwt") : String(assignment.assignmentJwt ?? ""),
|
|
135
|
+
};
|
|
136
|
+
});
|
|
119
137
|
|
|
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
138
|
return {
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
credential, transport, profile, fingerprint, metadata, status, catalog, assignModel,
|
|
140
|
+
close: transport.close,
|
|
141
|
+
defaultModelUid: Effect.sync(() => defaultModelUid),
|
|
142
|
+
accountIdentityHash: Effect.sync(() => accountIdentityHash),
|
|
143
|
+
models: Effect.map(catalog, (parsed) => parsed.models),
|
|
144
|
+
teamSettings: Effect.flatMap(metadata({ includeModelDisplays: true }), (value) =>
|
|
145
|
+
transport.unary("exa.seat_management_pb.SeatManagementService/GetCliTeamSettings", { metadata: value })),
|
|
146
|
+
// Only fetches the manifest. This does not mean installing a remote plugin or running its commands is approved.
|
|
147
|
+
managedPlugins: Effect.flatMap(metadata(), (value) => transport.unary("exa.api_server_pb.ApiServerService/GetAccountManagedPlugins", { metadata: value })),
|
|
148
|
+
self: Effect.suspend(() => transport.getJSON(restBaseURL, "/v3/self")),
|
|
149
|
+
integrations: Effect.suspend(() => organizationId
|
|
150
|
+
? transport.getJSON(restBaseURL, `/v3/organizations/${encodeURIComponent(organizationId)}/integrations`) : Effect.succeed(undefined)),
|
|
151
|
+
analytics: (events) => events.length === 0 ? Effect.void : Effect.asVoid(transport.unary("exa.product_analytics_pb.ProductAnalyticsService/BatchRecordAnalyticsEvents", {
|
|
152
|
+
events: [...events], apiKey: credential.sessionToken,
|
|
153
|
+
})),
|
|
131
154
|
};
|
|
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
|
-
}
|
|
155
|
+
});
|
|
150
156
|
}
|
|
151
157
|
|
|
152
|
-
|
|
153
158
|
export { getModelByUID, selectModel } from "./models/select.ts";
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { Data, Predicate } from "effect";
|
|
2
|
+
|
|
3
|
+
/** A failure whose message is safe to show. `code` keeps a Node or library error code, such as `ECONNRESET`, for classification. */
|
|
4
|
+
export class DevinError extends Data.TaggedError("DevinError")<{
|
|
5
|
+
readonly message: string;
|
|
6
|
+
readonly code?: string | undefined;
|
|
7
|
+
}> {}
|
|
8
|
+
|
|
9
|
+
/** Cancellation by the caller, the lifecycle or a closed client. Keeps the `AbortError` name hosts recognize. */
|
|
10
|
+
export class Cancelled extends Data.TaggedError("Cancelled")<{ readonly message: string }> {
|
|
11
|
+
override get name(): string { return "AbortError"; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** A local deadline or a host timeout. Keeps the `TimeoutError` name hosts recognize. */
|
|
15
|
+
export class RequestTimeout extends Data.TaggedError("RequestTimeout")<{ readonly message: string }> {
|
|
16
|
+
override get name(): string { return "TimeoutError"; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export type RPCFailureKind = "authentication" | "permission" | "rate_limit" | "timeout" | "rpc";
|
|
20
|
+
|
|
21
|
+
function rpcFailureKind(code: string, httpStatus: number | undefined): RPCFailureKind {
|
|
22
|
+
// An error body that contradicts the HTTP status does not invalidate authentication.
|
|
23
|
+
if (httpStatus !== undefined && httpStatus >= 400) {
|
|
24
|
+
if (httpStatus === 401) return "authentication";
|
|
25
|
+
if (httpStatus === 403) return "permission";
|
|
26
|
+
if (httpStatus === 429) return "rate_limit";
|
|
27
|
+
if (httpStatus === 408 || httpStatus === 504) return "timeout";
|
|
28
|
+
return "rpc";
|
|
29
|
+
}
|
|
30
|
+
if (code === "unauthenticated") return "authentication";
|
|
31
|
+
if (code === "permission_denied") return "permission";
|
|
32
|
+
if (code === "resource_exhausted") return "rate_limit";
|
|
33
|
+
if (code === "deadline_exceeded") return "timeout";
|
|
34
|
+
return "rpc";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const AUTHENTICATION_NOTICE = " The authentication credential is not valid. It may have expired or been revoked; sign in again with login.";
|
|
38
|
+
|
|
39
|
+
/** An error the server reported over HTTP or Connect. `message` is already `code: detail`. */
|
|
40
|
+
export class RPCError extends Data.TaggedError("RPCError")<{
|
|
41
|
+
readonly code: string;
|
|
42
|
+
readonly message: string;
|
|
43
|
+
readonly httpStatus?: number | undefined;
|
|
44
|
+
readonly retryAfterMs?: number | undefined;
|
|
45
|
+
}> {
|
|
46
|
+
static make(code: string, detail: string, httpStatus?: number, retryAfterMs?: number): RPCError {
|
|
47
|
+
const message = `${code}: ${detail}`;
|
|
48
|
+
return new RPCError({
|
|
49
|
+
code, httpStatus, retryAfterMs,
|
|
50
|
+
message: rpcFailureKind(code, httpStatus) === "authentication" ? message.slice(0, 600) + AUTHENTICATION_NOTICE : message,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
get kind(): RPCFailureKind { return rpcFailureKind(this.code, this.httpStatus); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export type DevinFailure = DevinError | Cancelled | RequestTimeout | RPCError;
|
|
58
|
+
|
|
59
|
+
const FAILURE_TAGS = new Set(["DevinError", "Cancelled", "RequestTimeout", "RPCError"]);
|
|
60
|
+
|
|
61
|
+
export function isDevinFailure(value: unknown): value is DevinFailure {
|
|
62
|
+
return value instanceof Error && Predicate.hasProperty(value, "_tag") && FAILURE_TAGS.has(value._tag as string);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The Node error code of a failure or of its cause, used to tell network failures apart. */
|
|
66
|
+
function errorCode(value: unknown): string | undefined {
|
|
67
|
+
if (Predicate.hasProperty(value, "code") && Predicate.isString(value.code)) return value.code;
|
|
68
|
+
const cause = value instanceof Error ? value.cause : undefined;
|
|
69
|
+
return Predicate.hasProperty(cause, "code") && Predicate.isString(cause.code) ? cause.code : undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Turns anything thrown or rejected by foreign code, including abort reasons, into a typed failure. */
|
|
73
|
+
export function toDevinFailure(cause: unknown): DevinFailure {
|
|
74
|
+
if (isDevinFailure(cause)) return cause;
|
|
75
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
76
|
+
const name = cause instanceof Error ? cause.name : undefined;
|
|
77
|
+
if (name === "AbortError") return new Cancelled({ message });
|
|
78
|
+
if (name === "TimeoutError") return new RequestTimeout({ message });
|
|
79
|
+
return new DevinError({ message, code: errorCode(cause) });
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Like `toDevinFailure` for synchronous library calls that cannot be cancelled: always a `DevinError`. */
|
|
83
|
+
export function toDevinError(cause: unknown): DevinError {
|
|
84
|
+
return cause instanceof DevinError ? cause : new DevinError({ message: cause instanceof Error ? cause.message : String(cause), code: errorCode(cause) });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function isAuthenticationError(error: unknown): error is RPCError {
|
|
88
|
+
return error instanceof RPCError && error.kind === "authentication";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const NETWORK_CODES = /^(ECONN|ENET|EHOST|EPIPE$|ENOTFOUND$|EAI_|ETIMEDOUT$|ERR_TLS_|CERT_|DEPTH_ZERO_SELF_SIGNED_CERT$|SELF_SIGNED_CERT_IN_CHAIN$|UNABLE_TO_)/;
|
|
92
|
+
const TRANSIENT_CONNECTION_CODES = /^(ECONNREFUSED|ECONNRESET|ECONNABORTED|EPIPE|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|ENETDOWN|ENOTFOUND|EAI_AGAIN|ERR_STREAM_PREMATURE_CLOSE)$/;
|
|
93
|
+
|
|
94
|
+
/** Failure classification for reports. Never includes a message, token or identifier. */
|
|
95
|
+
export function failureDetails(error: unknown): Record<string, unknown> {
|
|
96
|
+
const failure = toDevinFailure(error);
|
|
97
|
+
switch (failure._tag) {
|
|
98
|
+
case "RPCError": return {
|
|
99
|
+
category: failure.kind, code: failure.code,
|
|
100
|
+
...(failure.httpStatus === undefined ? {} : { httpStatus: failure.httpStatus }),
|
|
101
|
+
...(failure.retryAfterMs === undefined ? {} : { retryAfterMs: failure.retryAfterMs }),
|
|
102
|
+
...(failure.kind === "authentication" ? { requiresLogin: true } : {}),
|
|
103
|
+
};
|
|
104
|
+
case "RequestTimeout": return { category: "timeout", code: "deadline_exceeded" };
|
|
105
|
+
case "Cancelled": return { category: "cancelled" };
|
|
106
|
+
case "DevinError": return failure.code !== undefined && NETWORK_CODES.test(failure.code) ? { category: "network", code: failure.code } : { category: "error" };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The request got no usable answer from a working server, so sending it again may succeed. TLS and certificate failures are not included. */
|
|
111
|
+
export function isTransientConnectionFailure(error: unknown): boolean {
|
|
112
|
+
const failure = toDevinFailure(error);
|
|
113
|
+
if (failure instanceof RPCError) return failure.code === "unavailable" || [502, 503, 504].includes(failure.httpStatus ?? 0);
|
|
114
|
+
return failure instanceof DevinError && failure.code !== undefined && TRANSIENT_CONNECTION_CODES.test(failure.code);
|
|
115
|
+
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,12 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Host-independent API shared by the CLI and Pi. Version 2 returns Effect values instead of promises;
|
|
3
|
+
* version 3 takes the HTTP transport and device identity collection from services instead of options.
|
|
4
|
+
*/
|
|
5
|
+
export const CORE_CONTRACT_VERSION = 3;
|
|
3
6
|
export * from "./types.ts";
|
|
7
|
+
export * from "./errors.ts";
|
|
8
|
+
export * from "./cancellation.ts";
|
|
9
|
+
export * from "./validation.ts";
|
|
10
|
+
export * from "./redaction.ts";
|
|
4
11
|
export * from "./client.ts";
|
|
5
12
|
export * from "./auth/index.ts";
|
|
6
13
|
export * from "./models/catalog.ts";
|
|
7
|
-
export * from "./inference/
|
|
14
|
+
export * from "./inference/request.ts";
|
|
15
|
+
export * from "./inference/accumulator.ts";
|
|
16
|
+
export * from "./inference/stream-chat.ts";
|
|
8
17
|
export * from "./inference/messages.ts";
|
|
9
18
|
export * from "./inference/retry.ts";
|
|
10
19
|
export * from "./inference/state.ts";
|
|
11
|
-
export * from "./protocol/
|
|
20
|
+
export * from "./protocol/connect-client.ts";
|
|
21
|
+
export * from "./protocol/frames.ts";
|
|
22
|
+
export * from "./protocol/endpoint.ts";
|
|
12
23
|
export * from "./protocol/fingerprint.ts";
|
|
24
|
+
export * from "./services/http-transport.ts";
|
|
25
|
+
export * from "./services/device-identity.ts";
|
|
26
|
+
export * from "./services/platform.ts";
|