@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.
@@ -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 = /^sk-[A-Za-z0-9_-]{8,}$/u;
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-") ? "sk-proj" : "sk";
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, NotFoundError, PermissionDeniedError, RateLimitError, } from "openai";
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 [...new Set(response.data.map(modelIdFromItem).filter((model) => model !== undefined))]
62
- .sort((left, right) => left.localeCompare(right));
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, "Provider validation failed. Check the Base URL, API key, and selected model.");
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
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfey/tutti",
3
- "version": "0.1.43",
3
+ "version": "0.1.45",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",