@aexhq/sdk 0.54.1 → 0.55.1

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.
Files changed (57) hide show
  1. package/README.md +16 -123
  2. package/dist/errors.d.ts +33 -0
  3. package/dist/errors.js +69 -0
  4. package/dist/index.d.ts +16 -23
  5. package/dist/index.js +18 -16
  6. package/dist/json.d.ts +4 -0
  7. package/dist/json.js +46 -0
  8. package/dist/session.d.ts +74 -0
  9. package/dist/session.js +255 -0
  10. package/dist/transport.d.ts +22 -0
  11. package/dist/transport.js +196 -0
  12. package/package.json +16 -64
  13. package/LICENSE +0 -201
  14. package/NOTICE +0 -38
  15. package/dist/client/aex.d.ts +0 -38
  16. package/dist/client/aex.js +0 -149
  17. package/dist/client/aex.js.map +0 -1
  18. package/dist/client/credentials.d.ts +0 -25
  19. package/dist/client/credentials.js +0 -97
  20. package/dist/client/credentials.js.map +0 -1
  21. package/dist/client/routing.d.ts +0 -7
  22. package/dist/client/routing.js +0 -26
  23. package/dist/client/routing.js.map +0 -1
  24. package/dist/downloads/download.d.ts +0 -18
  25. package/dist/downloads/download.js +0 -67
  26. package/dist/downloads/download.js.map +0 -1
  27. package/dist/files/workspace-files.d.ts +0 -55
  28. package/dist/files/workspace-files.js +0 -150
  29. package/dist/files/workspace-files.js.map +0 -1
  30. package/dist/generated/errors.d.ts +0 -12
  31. package/dist/generated/errors.js +0 -66
  32. package/dist/generated/errors.js.map +0 -1
  33. package/dist/generated/models.d.ts +0 -853
  34. package/dist/generated/models.js +0 -62
  35. package/dist/generated/models.js.map +0 -1
  36. package/dist/generated/resources.d.ts +0 -265
  37. package/dist/generated/resources.js +0 -207
  38. package/dist/generated/resources.js.map +0 -1
  39. package/dist/generated/routes.d.ts +0 -44
  40. package/dist/generated/routes.js +0 -510
  41. package/dist/generated/routes.js.map +0 -1
  42. package/dist/index.js.map +0 -1
  43. package/dist/observations/stream.d.ts +0 -1
  44. package/dist/observations/stream.js +0 -18
  45. package/dist/observations/stream.js.map +0 -1
  46. package/dist/transport/errors.d.ts +0 -60
  47. package/dist/transport/errors.js +0 -107
  48. package/dist/transport/errors.js.map +0 -1
  49. package/dist/transport/pagination.d.ts +0 -8
  50. package/dist/transport/pagination.js +0 -34
  51. package/dist/transport/pagination.js.map +0 -1
  52. package/dist/transport/retry.d.ts +0 -21
  53. package/dist/transport/retry.js +0 -37
  54. package/dist/transport/retry.js.map +0 -1
  55. package/dist/transport/transport.d.ts +0 -33
  56. package/dist/transport/transport.js +0 -111
  57. package/dist/transport/transport.js.map +0 -1
package/README.md CHANGED
@@ -1,131 +1,24 @@
1
1
  # @aexhq/sdk
2
2
 
3
- The TypeScript SDK for Aex, with no dependencies of its own.
4
-
5
- ```bash
6
- npm install @aexhq/sdk
7
- ```
8
-
9
- ## Quickstart
10
-
11
3
  ```ts
12
- import { Aex, newId } from "@aexhq/sdk";
13
-
14
- const aex = new Aex({ apiKey: process.env.AEX_WORKSPACE_API_KEY! });
15
- const session = await aex.sessions.sessionCreate({
16
- body: {
17
- provider: "openai",
18
- model: process.env.AEX_MODEL!,
19
- providerApiKey: process.env.AEX_PROVIDER_API_KEY!,
4
+ import { Aex } from "@aexhq/sdk";
5
+ import { z } from "zod";
6
+
7
+ const aex = new Aex({ apiKey: "aex_sk_..." });
8
+ const session = await aex.sessions.create({
9
+ model: {
10
+ provider: "anthropic",
11
+ name: "claude-sonnet-5",
12
+ apiKey: "sk-ant-...",
20
13
  },
21
- idempotencyKey: crypto.randomUUID(),
22
14
  });
23
15
 
24
- const committed = (async () => {
25
- for await (const frame of aex.sessions.sessionMessagesStream({
26
- sessionId: session.id,
27
- })) {
28
- if (frame.kind === "committed" && frame.message?.role === "assistant") {
29
- return frame.message;
30
- }
31
- }
32
- throw new Error("message stream ended before the assistant committed");
33
- })();
34
-
35
- await aex.sessions.sessionMessageSend({
36
- sessionId: session.id,
37
- body: { text: "Inspect the tests and summarize the failures." },
38
- idempotencyKey: crypto.randomUUID(),
39
- });
40
- console.log(await committed);
41
-
42
- await aex.sessions.sessionTerminate({
43
- sessionId: session.id,
44
- body: {},
45
- operationId: newId("operation"),
46
- });
16
+ const result = await session.output(
17
+ z.object({ answer: z.string() }),
18
+ "Answer the question.",
19
+ );
47
20
  ```
48
21
 
49
- Creating a session takes the exact provider and model you want plus your own
50
- `providerApiKey`, which is write only. That key is encrypted for the session,
51
- never returned, and never shared with another one. You can use OpenAI,
52
- Anthropic, DeepSeek, xAI, Meta, Moonshot AI, and Alibaba.
53
-
54
- ## The client
55
-
56
- Session and file calls use a workspace `apiKey`. Account, API key, and billing
57
- calls use a `dashboardSession` from signing in with GitHub. One client can hold
58
- both, and it only ever sends each one to the service it belongs to.
59
-
60
- ```ts
61
- const aex = new Aex({
62
- apiKey: process.env.AEX_WORKSPACE_API_KEY!,
63
- dashboardSession: process.env.AEX_DASHBOARD_SESSION!,
64
- });
65
- ```
66
-
67
- The namespaces are `apiKeys`, `auth`, `billing`, `bootstrap`, `registry`,
68
- `sessions`, and `uploads`. `aex.workspaceFiles` adds friendlier helpers for
69
- working with files. `ROUTES` and `aex.execute(...)` give you the same generated
70
- contract directly, so there is no second route table to keep in step.
71
-
72
- ## Sessions and machines
73
-
74
- A session is the only thing you create. It holds the conversation, the work in
75
- flight, the tools it has used, its helper agents, its mounted files, its
76
- machine, and its telemetry.
77
-
78
- Every session gets a Linux machine unless you turn it off. It starts warming up
79
- as soon as the session is created and goes to sleep when nothing is running.
80
- The next tool call waits for it and wakes it if needed. Set `sandbox.enabled`
81
- to `false` to get no machine at all; tools that need one then return a clear
82
- error the model can handle.
83
-
84
- Cancel ends the current work and leaves the session idle. Terminate destroys
85
- the machine but keeps the messages and telemetry. Delete removes everything the
86
- session owns; files you uploaded to the workspace survive.
87
-
88
- ## Files, tools, and MCP
89
-
90
- `aex.workspaceFiles` gives you `put`, `upload`, `get`, `list`, `download`, and
91
- `delete`. Each name has one current value, which you can set from text or
92
- bytes, an HTTPS link, or a direct upload. Creating a session pins the names and
93
- paths it uses, so replacing a file later cannot change a session already
94
- running.
95
-
96
- Messages are text only. Upload images, PDFs, video, archives, or source, then
97
- tell the model which path under `/workspace` to look at.
98
-
99
- Out of the box the model gets Bash, file read, edit, and write, MCP,
100
- `storage.persist`, and tools to start, wait for, and stop helper agents. Attach
101
- MCP servers per session, either over HTTP or as a process on the machine. A
102
- large tool result leaves a short preview in the transcript and keeps the full
103
- bytes on the machine, and `storage.persist` saves a chosen file back to your
104
- workspace. A session can start at most 12 helper agents, nested no more than
105
- three deep.
106
-
107
- `sessionMessageSend` takes an optional `responseFormat` for JSON Schema output.
108
- Aex allows it only when the provider and model support it natively, and will
109
- not pass prompting off as the real thing.
110
-
111
- ## Streaming, telemetry, and billing
112
-
113
- `sessionMessagesStream` sends preview, gap, committed, reconcile, and heartbeat
114
- frames. Committed messages keep text, tool calls, and tool results in the same
115
- shape whichever provider you use.
116
-
117
- Telemetry comes as a live stream, a replay, and a verified compressed download.
118
- The billing methods cover your prepaid balance, saved card details, hosted card
119
- setup and top up, a permanent record of transactions, and usage. Billing needs
120
- `dashboardSession`; sessions and files need `apiKey`.
121
-
122
- See the [Aex documentation](https://aex.dev/docs) for fuller examples.
123
-
124
- ## Status
125
-
126
- Aex is in active prelaunch development. Expect breaking changes, and do not
127
- rely on the hosted service for production workloads yet.
128
-
129
- ## License
130
-
131
- Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
22
+ `send()` returns text. `output()` returns a normal typed Promise and uses `https://api.aex.dev` by
23
+ default. Zod schemas must be representable as JSON Schema; process-local custom refinements and
24
+ transforms fail before any model work is admitted.
@@ -0,0 +1,33 @@
1
+ import type { ApiError, ApiErrorCode, OutputValidationIssue } from "@aexhq/contracts/session";
2
+ export interface AexErrorOptions {
3
+ code?: ApiErrorCode | undefined;
4
+ status?: number | undefined;
5
+ param?: string | undefined;
6
+ requestId?: string | undefined;
7
+ cause?: unknown;
8
+ }
9
+ export declare class AexError extends Error {
10
+ readonly code: ApiErrorCode | undefined;
11
+ readonly status: number | undefined;
12
+ readonly param: string | undefined;
13
+ readonly requestId: string | undefined;
14
+ constructor(message: string, options?: AexErrorOptions);
15
+ }
16
+ export declare class OutputSchemaError extends AexError {
17
+ constructor(message: string, options?: AexErrorOptions);
18
+ }
19
+ export declare class OutputRefusalError extends AexError {
20
+ constructor(message: string, options?: AexErrorOptions);
21
+ }
22
+ export declare class OutputValidationError extends AexError {
23
+ readonly issues: readonly OutputValidationIssue[];
24
+ constructor(message: string, issues?: readonly OutputValidationIssue[], options?: AexErrorOptions);
25
+ }
26
+ export declare class SessionError extends AexError {
27
+ constructor(message: string, options?: AexErrorOptions);
28
+ }
29
+ export declare class AbortError extends AexError {
30
+ constructor(message?: string, options?: AexErrorOptions);
31
+ }
32
+ export declare function errorFromApi(error: ApiError, status?: number, issues?: readonly OutputValidationIssue[]): AexError;
33
+ export declare function abortError(cause?: unknown): AbortError;
package/dist/errors.js ADDED
@@ -0,0 +1,69 @@
1
+ export class AexError extends Error {
2
+ code;
3
+ status;
4
+ param;
5
+ requestId;
6
+ constructor(message, options = {}) {
7
+ super(message, options.cause === undefined ? undefined : { cause: options.cause });
8
+ this.name = "AexError";
9
+ this.code = options.code;
10
+ this.status = options.status;
11
+ this.param = options.param;
12
+ this.requestId = options.requestId;
13
+ }
14
+ }
15
+ export class OutputSchemaError extends AexError {
16
+ constructor(message, options = {}) {
17
+ super(message, { ...options, code: "output_schema_error" });
18
+ this.name = "OutputSchemaError";
19
+ }
20
+ }
21
+ export class OutputRefusalError extends AexError {
22
+ constructor(message, options = {}) {
23
+ super(message, { ...options, code: "output_refused" });
24
+ this.name = "OutputRefusalError";
25
+ }
26
+ }
27
+ export class OutputValidationError extends AexError {
28
+ issues;
29
+ constructor(message, issues = [], options = {}) {
30
+ super(message, { ...options, code: "output_validation_error" });
31
+ this.name = "OutputValidationError";
32
+ this.issues = issues;
33
+ }
34
+ }
35
+ export class SessionError extends AexError {
36
+ constructor(message, options = {}) {
37
+ super(message, options);
38
+ this.name = "SessionError";
39
+ }
40
+ }
41
+ export class AbortError extends AexError {
42
+ constructor(message = "The operation was cancelled", options = {}) {
43
+ super(message, { ...options, code: "cancelled" });
44
+ this.name = "AbortError";
45
+ }
46
+ }
47
+ export function errorFromApi(error, status, issues = []) {
48
+ const options = {
49
+ code: error.code,
50
+ status,
51
+ param: error.param,
52
+ requestId: error.request_id,
53
+ };
54
+ switch (error.code) {
55
+ case "output_schema_error":
56
+ return new OutputSchemaError(error.message, options);
57
+ case "output_refused":
58
+ return new OutputRefusalError(error.message, options);
59
+ case "output_validation_error":
60
+ return new OutputValidationError(error.message, issues, options);
61
+ case "cancelled":
62
+ return new AbortError(error.message, options);
63
+ default:
64
+ return new SessionError(error.message, options);
65
+ }
66
+ }
67
+ export function abortError(cause) {
68
+ return new AbortError("The operation was cancelled", { cause });
69
+ }
package/dist/index.d.ts CHANGED
@@ -1,23 +1,16 @@
1
- export { Aex } from "./client/aex.js";
2
- export type { AexOptions } from "./client/aex.js";
3
- export { DashboardSession, WorkspaceApiKey, parseCredential, regionalHost, } from "./client/credentials.js";
4
- export type { ParsedCredential, RegionCode } from "./client/credentials.js";
5
- export { resolveCentralBaseUrl, resolveRegionalBaseUrl } from "./client/routing.js";
6
- export type { RegionalRoutingOptions } from "./client/routing.js";
7
- export { AexApiError, AexAuthError, AexConfigError, AexConflictError, AexError, AexGoneError, AexInternalError, AexNotFoundError, AexPreconditionError, AexQuotaError, AexStateError, AexStreamProtocolError, AexUnavailableError, AexValidationError, apiErrorFromResponse, isRetryable, } from "./transport/errors.js";
8
- export type { AexErrorCode } from "./transport/errors.js";
9
- export { RETRY_POLICY, executeWithRetry } from "./transport/retry.js";
10
- export { FetchTransport } from "./transport/transport.js";
11
- export type { AexTransport, WireRequest, WireResponse, WireStreamResponse } from "./transport/transport.js";
12
- export { Page } from "./transport/pagination.js";
13
- export { WorkspaceFiles } from "./files/workspace-files.js";
14
- export type { CurrentWorkspaceFile, WorkspaceFileInput, WorkspaceFilePutOptions, } from "./files/workspace-files.js";
15
- export { Download, MAX_SINGLE_GET_BYTES, planDownloadRanges } from "./downloads/download.js";
16
- export type { DownloadRange } from "./downloads/download.js";
17
- export { parseNdjsonFrames } from "./observations/stream.js";
18
- export { ERROR_METADATA } from "./generated/errors.js";
19
- export type { ErrorClass } from "./generated/errors.js";
20
- export { ROUTES } from "./generated/routes.js";
21
- export type { RouteDescriptor, RouteId } from "./generated/routes.js";
22
- export * from "./generated/models.js";
23
- export type { ExecuteOptions, ResourceExecutor } from "./generated/resources.js";
1
+ import { Sessions } from "./session.js";
2
+ import type { Fetch } from "./transport.js";
3
+ export { AbortError, AexError, OutputRefusalError, OutputSchemaError, OutputValidationError, SessionError, } from "./errors.js";
4
+ export type { AexErrorOptions } from "./errors.js";
5
+ export { Session, Sessions, } from "./session.js";
6
+ export type { CreateSessionOptions, ListSessionsOptions, ModelSummary, ModelOptions, OutputOptions, RequestOptions, SessionInput, SessionList, SessionSummary, } from "./session.js";
7
+ export type { EventOptions } from "./transport.js";
8
+ export interface AexOptions {
9
+ apiKey: string;
10
+ baseUrl?: string;
11
+ fetch?: Fetch;
12
+ }
13
+ export declare class Aex {
14
+ readonly sessions: Sessions;
15
+ constructor(options: AexOptions);
16
+ }
package/dist/index.js CHANGED
@@ -1,16 +1,18 @@
1
- export { Aex } from "./client/aex.js";
2
- export { DashboardSession, WorkspaceApiKey, parseCredential, regionalHost, } from "./client/credentials.js";
3
- export { resolveCentralBaseUrl, resolveRegionalBaseUrl } from "./client/routing.js";
4
- export { AexApiError, AexAuthError, AexConfigError, AexConflictError, AexError, AexGoneError, AexInternalError, AexNotFoundError, AexPreconditionError, AexQuotaError, AexStateError, AexStreamProtocolError, AexUnavailableError, AexValidationError, apiErrorFromResponse, isRetryable, } from "./transport/errors.js";
5
- export { RETRY_POLICY, executeWithRetry } from "./transport/retry.js";
6
- export { FetchTransport } from "./transport/transport.js";
7
- export { Page } from "./transport/pagination.js";
8
- export { WorkspaceFiles } from "./files/workspace-files.js";
9
- export { Download, MAX_SINGLE_GET_BYTES, planDownloadRanges } from "./downloads/download.js";
10
- export { parseNdjsonFrames } from "./observations/stream.js";
11
- export { ERROR_METADATA } from "./generated/errors.js";
12
- export { ROUTES } from "./generated/routes.js";
13
- // Models and identifier constructors are emitted from the authored contract.
14
- // Re-exporting them here keeps application code on one dependency-free package.
15
- export * from "./generated/models.js";
16
- //# sourceMappingURL=index.js.map
1
+ import { Sessions } from "./session.js";
2
+ import { Transport } from "./transport.js";
3
+ export { AbortError, AexError, OutputRefusalError, OutputSchemaError, OutputValidationError, SessionError, } from "./errors.js";
4
+ export { Session, Sessions, } from "./session.js";
5
+ const DEFAULT_API_URL = "https://api.aex.dev";
6
+ export class Aex {
7
+ sessions;
8
+ constructor(options) {
9
+ if (options.apiKey.trim() === "")
10
+ throw new TypeError("Aex apiKey cannot be empty");
11
+ const fetchImplementation = options.fetch ?? globalThis.fetch?.bind(globalThis);
12
+ if (fetchImplementation === undefined) {
13
+ throw new TypeError("This runtime does not provide fetch; pass a fetch implementation to Aex");
14
+ }
15
+ const transport = new Transport(options.apiKey, options.baseUrl ?? DEFAULT_API_URL, fetchImplementation);
16
+ this.sessions = new Sessions(transport);
17
+ }
18
+ }
package/dist/json.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** RFC 8785 canonical JSON. Object keys sort by UTF-16 code units, as required by JCS. */
2
+ export declare function canonicalize(value: unknown): string;
3
+ export declare function jcsSha256(value: unknown): Promise<string>;
4
+ export declare function randomIdempotencyKey(): string;
package/dist/json.js ADDED
@@ -0,0 +1,46 @@
1
+ import { OutputSchemaError, SessionError } from "./errors.js";
2
+ /** RFC 8785 canonical JSON. Object keys sort by UTF-16 code units, as required by JCS. */
3
+ export function canonicalize(value) {
4
+ const visit = (current) => {
5
+ if (Array.isArray(current))
6
+ return current.map(visit);
7
+ if (current !== null && typeof current === "object") {
8
+ const object = current;
9
+ return Object.fromEntries(Object.keys(object)
10
+ .filter((key) => object[key] !== undefined)
11
+ .sort()
12
+ .map((key) => [key, visit(object[key])]));
13
+ }
14
+ if (typeof current === "number" && !Number.isFinite(current)) {
15
+ throw new OutputSchemaError("The output schema contains a non-finite number");
16
+ }
17
+ return current;
18
+ };
19
+ const encoded = JSON.stringify(visit(value));
20
+ if (encoded === undefined) {
21
+ throw new OutputSchemaError("The output schema is not JSON-serialisable");
22
+ }
23
+ return encoded;
24
+ }
25
+ export async function jcsSha256(value) {
26
+ const subtle = globalThis.crypto?.subtle;
27
+ if (subtle === undefined) {
28
+ throw new OutputSchemaError("This runtime does not provide Web Crypto SHA-256 support");
29
+ }
30
+ const bytes = new TextEncoder().encode(canonicalize(value));
31
+ const digest = await subtle.digest("SHA-256", bytes);
32
+ return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
33
+ }
34
+ export function randomIdempotencyKey() {
35
+ const crypto = globalThis.crypto;
36
+ if (crypto === undefined) {
37
+ throw new SessionError("This runtime does not provide secure random values");
38
+ }
39
+ if (typeof crypto.randomUUID === "function")
40
+ return crypto.randomUUID();
41
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
42
+ bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
43
+ bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
44
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
45
+ return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
46
+ }
@@ -0,0 +1,74 @@
1
+ import type { Event, Provider, Session as SessionData, SessionState } from "@aexhq/contracts/session";
2
+ import * as z from "zod";
3
+ import type { EventOptions } from "./transport.js";
4
+ import { Transport } from "./transport.js";
5
+ export type SessionInput = string;
6
+ export interface ModelOptions {
7
+ provider: Provider;
8
+ name: string;
9
+ apiKey: string;
10
+ baseUrl?: string;
11
+ maxOutputTokens?: number;
12
+ temperature?: number;
13
+ reasoningEffort?: "low" | "medium" | "high";
14
+ }
15
+ export interface CreateSessionOptions {
16
+ model: ModelOptions;
17
+ systemPrompt?: string;
18
+ metadata?: Record<string, string>;
19
+ }
20
+ export interface RequestOptions {
21
+ signal?: AbortSignal;
22
+ idempotencyKey?: string;
23
+ metadata?: Record<string, string>;
24
+ }
25
+ export interface OutputOptions extends RequestOptions {
26
+ }
27
+ export interface ListSessionsOptions {
28
+ limit?: number;
29
+ cursor?: string;
30
+ state?: SessionState;
31
+ signal?: AbortSignal;
32
+ }
33
+ export interface SessionList {
34
+ data: Session[];
35
+ hasMore: boolean;
36
+ nextCursor?: string;
37
+ }
38
+ export interface ModelSummary {
39
+ provider: Provider;
40
+ name: string;
41
+ baseUrl?: string;
42
+ }
43
+ export interface SessionSummary {
44
+ id: string;
45
+ state: SessionState;
46
+ model: ModelSummary;
47
+ createdAt: string;
48
+ updatedAt: string;
49
+ metadata: Readonly<Record<string, string | undefined>>;
50
+ }
51
+ export declare class Sessions {
52
+ #private;
53
+ constructor(transport: Transport);
54
+ create(options: CreateSessionOptions, request?: RequestOptions): Promise<Session>;
55
+ get(id: string, options?: Pick<RequestOptions, "signal">): Promise<Session>;
56
+ list(options?: ListSessionsOptions): Promise<SessionList>;
57
+ }
58
+ export declare class Session implements SessionSummary {
59
+ #private;
60
+ constructor(transport: Transport, data: SessionData);
61
+ get id(): string;
62
+ get state(): SessionState;
63
+ get model(): ModelSummary;
64
+ get createdAt(): string;
65
+ get updatedAt(): string;
66
+ get metadata(): Readonly<Record<string, string | undefined>>;
67
+ refresh(options?: Pick<RequestOptions, "signal">): Promise<this>;
68
+ send(input: SessionInput, options?: RequestOptions): Promise<string>;
69
+ output<Schema extends z.ZodType>(schema: Schema, input?: SessionInput, options?: OutputOptions): Promise<z.output<Schema>>;
70
+ events(options?: EventOptions): AsyncGenerator<Event>;
71
+ cancel(options?: Pick<RequestOptions, "signal">): Promise<this>;
72
+ delete(options?: Pick<RequestOptions, "signal">): Promise<void>;
73
+ private markIdle;
74
+ }