@xfey/tutti 0.1.43 → 0.1.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/providers/openai/credential-store.js +6 -2
- package/dist/providers/openai/credential-validation.js +4 -7
- package/dist/server-shell/cli/launch.d.ts +2 -0
- package/dist/server-shell/cli/launch.js +7 -1
- package/dist/server-shell/local-console/project-service.d.ts +11 -0
- package/dist/server-shell/local-console/project-service.js +43 -1
- package/dist/server-shell/local-console/server.js +10 -0
- package/package.json +1 -1
- package/web/assets/index-B_3ILIDb.js +69 -0
- package/web/assets/index-RBTs-6vt.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-CT68KthI.js +0 -29
- package/web/assets/index-Sqw_t67u.css +0 -1
|
@@ -5,7 +5,7 @@ import { REDACTION_LABELS } from "@tutti/shared/utils";
|
|
|
5
5
|
import { OPENAI_API_KEY_AUTH_KIND, OPENAI_PROVIDER } from "./model-config.js";
|
|
6
6
|
import { readJsonFile, writePrivateJsonFile } from "./machine-local-files.js";
|
|
7
7
|
const CREDENTIAL_REF_PATTERN = /^cred_[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
|
|
8
|
-
const OPENAI_API_KEY_PATTERN = /^
|
|
8
|
+
const OPENAI_API_KEY_PATTERN = /^[\x21-\x7E]{8,4096}$/u;
|
|
9
9
|
function isRecord(value) {
|
|
10
10
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
11
|
}
|
|
@@ -35,7 +35,11 @@ export function createRedactedOpenAiApiKeyLabel(apiKey) {
|
|
|
35
35
|
if (!isOpenAiApiKey(apiKey)) {
|
|
36
36
|
return REDACTION_LABELS.providerApiKey;
|
|
37
37
|
}
|
|
38
|
-
const prefix = apiKey.startsWith("sk-proj-")
|
|
38
|
+
const prefix = apiKey.startsWith("sk-proj-")
|
|
39
|
+
? "sk-proj"
|
|
40
|
+
: apiKey.startsWith("sk-")
|
|
41
|
+
? "sk"
|
|
42
|
+
: "key";
|
|
39
43
|
return `${prefix}...${apiKey.slice(-4)}`;
|
|
40
44
|
}
|
|
41
45
|
export function resolveOpenAiCredentialPath(tuttiHome, projectId, credentialRef) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import OpenAI, { APIConnectionError, APIConnectionTimeoutError, APIError, AuthenticationError,
|
|
1
|
+
import OpenAI, { APIConnectionError, APIConnectionTimeoutError, APIError, AuthenticationError, PermissionDeniedError, RateLimitError, } from "openai";
|
|
2
2
|
import { DEFAULT_OPENAI_MODEL } from "./model-config.js";
|
|
3
3
|
const VALIDATION_INPUT = "Tutti provider validation. Reply with the single word ok.";
|
|
4
4
|
const VALIDATION_TIMEOUT_MS = 20_000;
|
|
@@ -58,8 +58,9 @@ function modelIdsFromResponse(response) {
|
|
|
58
58
|
if (!isRecord(response) || !Array.isArray(response.data)) {
|
|
59
59
|
return [];
|
|
60
60
|
}
|
|
61
|
-
return [
|
|
62
|
-
.
|
|
61
|
+
return [
|
|
62
|
+
...new Set(response.data.map(modelIdFromItem).filter((model) => model !== undefined)),
|
|
63
|
+
].sort((left, right) => left.localeCompare(right));
|
|
63
64
|
}
|
|
64
65
|
function lowerErrorText(error) {
|
|
65
66
|
if (error instanceof Error) {
|
|
@@ -158,10 +159,6 @@ function looksLikeQuotaOrBillingFailure(error) {
|
|
|
158
159
|
message.includes("payment"));
|
|
159
160
|
}
|
|
160
161
|
function looksLikeModelUnavailable(error) {
|
|
161
|
-
const status = apiErrorStatus(error);
|
|
162
|
-
if (status === 404 || error instanceof NotFoundError) {
|
|
163
|
-
return true;
|
|
164
|
-
}
|
|
165
162
|
const code = apiErrorCode(error);
|
|
166
163
|
if (code === "model_not_found" || code === "model_not_available") {
|
|
167
164
|
return true;
|
|
@@ -11,6 +11,8 @@ export type LaunchPreparationOptions = {
|
|
|
11
11
|
now?: () => Date;
|
|
12
12
|
confirm?: LaunchConfirmationHandler;
|
|
13
13
|
allowWorkspaceRootTakeover?: boolean;
|
|
14
|
+
projectDisplayName?: string;
|
|
15
|
+
projectDescription?: string;
|
|
14
16
|
};
|
|
15
17
|
export type LaunchProviderStatus = OpenAiProviderConfigProjection["status"];
|
|
16
18
|
export type LaunchPreparationResult = {
|
|
@@ -4,7 +4,7 @@ import { readOpenAiProviderConfigProjection, resolveTuttiHome, } from "../../pro
|
|
|
4
4
|
import { LaunchError } from "./errors.js";
|
|
5
5
|
import { assertSafeProjectRoot, ensureGitBootstrap, } from "./git-bootstrap.js";
|
|
6
6
|
import { ensureMachineProjectBinding } from "./machine-local.js";
|
|
7
|
-
import { readProjectIdentity, readProjectDisplayName, writeProjectIdentity, } from "./project-identity.js";
|
|
7
|
+
import { readProjectIdentity, readProjectDisplayName, writeProjectDescription, writeProjectDisplayName, writeProjectIdentity, } from "./project-identity.js";
|
|
8
8
|
const DEFAULT_RELAY_URL = "https://tutti.now";
|
|
9
9
|
const DEFAULT_HOST_ENDPOINT_HOST = "127.0.0.1";
|
|
10
10
|
const AUTO_HOST_ENDPOINT_PORT = 0;
|
|
@@ -64,6 +64,12 @@ export async function prepareLaunchProject(options) {
|
|
|
64
64
|
gitOptions.confirm = options.confirm;
|
|
65
65
|
}
|
|
66
66
|
const git = await ensureGitBootstrap(gitOptions);
|
|
67
|
+
if (projectIdentityCreated && options.projectDisplayName !== undefined) {
|
|
68
|
+
writeProjectDisplayName(workspaceRoot, options.projectDisplayName);
|
|
69
|
+
}
|
|
70
|
+
if (projectIdentityCreated && options.projectDescription !== undefined) {
|
|
71
|
+
writeProjectDescription(workspaceRoot, options.projectDescription);
|
|
72
|
+
}
|
|
67
73
|
const bindingOptions = {
|
|
68
74
|
tuttiHome,
|
|
69
75
|
projectId,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ProjectId, type RelayProjectRef } from "@tutti/shared/ids";
|
|
2
|
+
import { type ConfigureProjectOpenAiProviderResult } from "../../providers/openai/index.js";
|
|
2
3
|
import { type FetchLike } from "../cli/host-runtime-endpoint.js";
|
|
3
4
|
import { type RuntimeProjectRow } from "../cli/runtime-commands.js";
|
|
4
5
|
import { type LocalConsoleInvocationContext } from "./invocation-context.js";
|
|
@@ -23,6 +24,10 @@ export type LocalConsoleProviderInput = {
|
|
|
23
24
|
api_key: string;
|
|
24
25
|
model: string;
|
|
25
26
|
};
|
|
27
|
+
export type LocalConsoleProjectMetadataInput = {
|
|
28
|
+
display_name: string;
|
|
29
|
+
description?: string;
|
|
30
|
+
};
|
|
26
31
|
export type LocalConsoleLaunchResult = {
|
|
27
32
|
project: LocalConsoleProject;
|
|
28
33
|
project_created: boolean;
|
|
@@ -39,6 +44,10 @@ export declare class LocalConsoleProjectError extends Error {
|
|
|
39
44
|
readonly statusCode: number;
|
|
40
45
|
constructor(code: string, message: string, statusCode?: number);
|
|
41
46
|
}
|
|
47
|
+
type LocalConsoleProviderValidationFailure = Extract<ConfigureProjectOpenAiProviderResult, {
|
|
48
|
+
kind: "validation_failed";
|
|
49
|
+
}>;
|
|
50
|
+
export declare function localConsoleProviderValidationMessage(result: LocalConsoleProviderValidationFailure): string;
|
|
42
51
|
export declare class LocalConsoleProjectService {
|
|
43
52
|
#private;
|
|
44
53
|
constructor(options: {
|
|
@@ -59,6 +68,7 @@ export declare class LocalConsoleProjectService {
|
|
|
59
68
|
}>;
|
|
60
69
|
launchProject(input: {
|
|
61
70
|
workspacePath: string;
|
|
71
|
+
project?: LocalConsoleProjectMetadataInput;
|
|
62
72
|
provider?: LocalConsoleProviderInput;
|
|
63
73
|
}, context: LocalConsoleInvocationContext): Promise<LocalConsoleLaunchResult>;
|
|
64
74
|
openProject(projectId: string, context: LocalConsoleInvocationContext): Promise<LocalConsoleOpenResult>;
|
|
@@ -70,4 +80,5 @@ export declare class LocalConsoleProjectService {
|
|
|
70
80
|
message: string;
|
|
71
81
|
}>;
|
|
72
82
|
}
|
|
83
|
+
export {};
|
|
73
84
|
//# sourceMappingURL=project-service.d.ts.map
|
|
@@ -23,6 +23,23 @@ export class LocalConsoleProjectError extends Error {
|
|
|
23
23
|
this.name = "LocalConsoleProjectError";
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
|
+
export function localConsoleProviderValidationMessage(result) {
|
|
27
|
+
switch (result.reason) {
|
|
28
|
+
case "provider_auth_invalid":
|
|
29
|
+
return "The API key was rejected. Enter the raw bearer token and try again.";
|
|
30
|
+
case "provider_model_unavailable":
|
|
31
|
+
return "The selected model is not available through this Responses API endpoint.";
|
|
32
|
+
case "provider_quota_or_billing_required":
|
|
33
|
+
return "This Provider account does not have available quota or billing access.";
|
|
34
|
+
case "provider_rate_limited":
|
|
35
|
+
return "The Provider is rate limited. Wait a moment, then try again.";
|
|
36
|
+
case "provider_network_error":
|
|
37
|
+
if (result.diagnostic?.http_status === 404 || result.diagnostic?.http_status === 405) {
|
|
38
|
+
return "This Base URL does not expose an OpenAI-compatible POST /responses endpoint.";
|
|
39
|
+
}
|
|
40
|
+
return "Could not validate the OpenAI Responses API endpoint. Check the Base URL and try again.";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
26
43
|
function validateWorkspacePath(value) {
|
|
27
44
|
const path = resolve(value.trim());
|
|
28
45
|
if (value.trim() === "" || !existsSync(path) || !statSync(path).isDirectory()) {
|
|
@@ -50,6 +67,20 @@ function validateProvider(input) {
|
|
|
50
67
|
}
|
|
51
68
|
return provider;
|
|
52
69
|
}
|
|
70
|
+
function validateProjectMetadata(input) {
|
|
71
|
+
const displayName = input.display_name.trim();
|
|
72
|
+
const description = input.description?.trim();
|
|
73
|
+
if (displayName === "" || displayName.length > 80) {
|
|
74
|
+
throw new LocalConsoleProjectError("project_display_name_invalid", "Project name must be between 1 and 80 characters.");
|
|
75
|
+
}
|
|
76
|
+
if (description !== undefined && description.length > 160) {
|
|
77
|
+
throw new LocalConsoleProjectError("project_description_invalid", "Project description cannot exceed 160 characters.");
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
display_name: displayName,
|
|
81
|
+
...(description === undefined || description === "" ? {} : { description }),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
53
84
|
function projectFromRuntimeRow(row, providerStatus, providerModel, metadata) {
|
|
54
85
|
const currentVersion = readCliVersion();
|
|
55
86
|
const openUrl = projectOpenUrl(metadata.relayUrl, row.relay_project_ref);
|
|
@@ -220,6 +251,9 @@ export class LocalConsoleProjectService {
|
|
|
220
251
|
}
|
|
221
252
|
const launch = this.#runMutation(async () => await this.#launchProject({
|
|
222
253
|
workspacePath,
|
|
254
|
+
...(input.project === undefined
|
|
255
|
+
? {}
|
|
256
|
+
: { project: validateProjectMetadata(input.project) }),
|
|
223
257
|
...(input.provider === undefined ? {} : { provider: validateProvider(input.provider) }),
|
|
224
258
|
}, context)).finally(() => {
|
|
225
259
|
this.#launches.delete(workspacePath);
|
|
@@ -234,6 +268,14 @@ export class LocalConsoleProjectService {
|
|
|
234
268
|
preparation = await prepareLaunchProject({
|
|
235
269
|
workspacePath: input.workspacePath,
|
|
236
270
|
yes: true,
|
|
271
|
+
...(input.project === undefined
|
|
272
|
+
? {}
|
|
273
|
+
: {
|
|
274
|
+
projectDisplayName: input.project.display_name,
|
|
275
|
+
...(input.project.description === undefined
|
|
276
|
+
? {}
|
|
277
|
+
: { projectDescription: input.project.description }),
|
|
278
|
+
}),
|
|
237
279
|
...operation,
|
|
238
280
|
});
|
|
239
281
|
}
|
|
@@ -249,7 +291,7 @@ export class LocalConsoleProjectService {
|
|
|
249
291
|
defaultModel: input.provider.model,
|
|
250
292
|
});
|
|
251
293
|
if (configured.kind === "validation_failed") {
|
|
252
|
-
throw new LocalConsoleProjectError(configured.reason,
|
|
294
|
+
throw new LocalConsoleProjectError(configured.reason, localConsoleProviderValidationMessage(configured));
|
|
253
295
|
}
|
|
254
296
|
}
|
|
255
297
|
else if (preparation.provider_status !== "configured") {
|
|
@@ -256,6 +256,15 @@ export function createLocalConsoleServer(options) {
|
|
|
256
256
|
additionalProperties: false,
|
|
257
257
|
properties: {
|
|
258
258
|
workspace_path: { type: "string", minLength: 1, maxLength: 4096 },
|
|
259
|
+
project: {
|
|
260
|
+
type: "object",
|
|
261
|
+
required: ["display_name"],
|
|
262
|
+
additionalProperties: false,
|
|
263
|
+
properties: {
|
|
264
|
+
display_name: { type: "string", minLength: 1, maxLength: 80 },
|
|
265
|
+
description: { type: "string", maxLength: 160 },
|
|
266
|
+
},
|
|
267
|
+
},
|
|
259
268
|
provider: {
|
|
260
269
|
type: "object",
|
|
261
270
|
required: ["base_url", "api_key", "model"],
|
|
@@ -273,6 +282,7 @@ export function createLocalConsoleServer(options) {
|
|
|
273
282
|
const session = requireSession(request, true);
|
|
274
283
|
return await projects.launchProject({
|
|
275
284
|
workspacePath: request.body.workspace_path,
|
|
285
|
+
...(request.body.project === undefined ? {} : { project: request.body.project }),
|
|
276
286
|
...(request.body.provider === undefined ? {} : { provider: request.body.provider }),
|
|
277
287
|
}, session.context);
|
|
278
288
|
});
|