@fugood/buttress-server 2.25.0-beta.9 → 2.25.1-beta.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.
Files changed (72) hide show
  1. package/README.md +506 -35
  2. package/config/function-samples/README.md +37 -0
  3. package/config/function-samples/_auth.ts +48 -0
  4. package/config/function-samples/host-info.ts +25 -0
  5. package/config/function-samples/summarize-text.ts +55 -0
  6. package/config/function-samples/text-to-speech.ts +51 -0
  7. package/config/function-samples/transcribe-media.ts +65 -0
  8. package/config/sample.toml +25 -0
  9. package/lib/autodiscover/index.d.ts +20 -0
  10. package/lib/autodiscover/sign.d.ts +10 -0
  11. package/lib/autodiscover/types.d.ts +32 -0
  12. package/lib/autodiscover/udp.d.ts +22 -0
  13. package/lib/cli.d.ts +3 -0
  14. package/lib/functions/auth.d.ts +28 -0
  15. package/lib/functions/config.d.ts +20 -0
  16. package/lib/functions/constants.d.ts +17 -0
  17. package/lib/functions/executor.d.ts +27 -0
  18. package/lib/functions/files.d.ts +29 -0
  19. package/lib/functions/index.d.ts +50 -0
  20. package/lib/functions/libs.d.ts +10 -0
  21. package/lib/functions/loader.d.ts +48 -0
  22. package/lib/functions/mcp.d.ts +35 -0
  23. package/lib/functions/registry.d.ts +34 -0
  24. package/lib/functions/scaffold.d.ts +18 -0
  25. package/lib/functions/status.d.ts +119 -0
  26. package/lib/functions/templates.d.ts +14 -0
  27. package/lib/functions/transpile.d.ts +23 -0
  28. package/lib/functions/types.d.ts +196 -0
  29. package/lib/functions/uploads.d.ts +42 -0
  30. package/lib/functions/watcher.d.ts +34 -0
  31. package/lib/index.d.ts +36 -0
  32. package/lib/index.mjs +1539 -64
  33. package/lib/package.d.ts +7 -0
  34. package/lib/routes/anthropic-messages.d.ts +55 -0
  35. package/lib/routes/file.d.ts +10 -0
  36. package/lib/routes/functions.check.d.ts +13 -0
  37. package/lib/routes/functions.d.ts +16 -0
  38. package/lib/routes/generator-cache.d.ts +42 -0
  39. package/lib/routes/index.d.ts +6 -0
  40. package/lib/routes/info.check.d.ts +1 -0
  41. package/lib/routes/info.d.ts +4 -0
  42. package/lib/routes/llm-shared.d.ts +57 -0
  43. package/lib/routes/openai-compat.d.ts +17 -0
  44. package/lib/routes/status.d.ts +4 -0
  45. package/lib/routes/stt-shared.d.ts +36 -0
  46. package/lib/routes/tts-shared.d.ts +36 -0
  47. package/lib/services/common.d.ts +29 -0
  48. package/lib/services/create-llm-service.d.ts +24 -0
  49. package/lib/services/create-onnx-init-context.d.ts +10 -0
  50. package/lib/services/ggml-llm.d.ts +5 -0
  51. package/lib/services/ggml-stt.d.ts +26 -0
  52. package/lib/services/index.d.ts +39 -0
  53. package/lib/services/mlx-llm.d.ts +5 -0
  54. package/lib/services/onnx-stt.d.ts +77 -0
  55. package/lib/services/onnx-tts.d.ts +48 -0
  56. package/lib/types.d.ts +181 -0
  57. package/lib/utils/SessionFileManager.d.ts +16 -0
  58. package/lib/utils/buttressAuth.d.ts +25 -0
  59. package/lib/utils/config.d.ts +21 -0
  60. package/lib/utils/functionsAuthGuard.d.ts +12 -0
  61. package/lib/utils/httpAuthGuard.d.ts +1 -0
  62. package/lib/utils/net.d.ts +6 -0
  63. package/lib/utils/router.d.ts +2 -0
  64. package/lib/utils/serialize.d.ts +2 -0
  65. package/lib/utils/serverCaps.d.ts +4 -0
  66. package/lib/utils/sessionGuard.d.ts +33 -0
  67. package/lib/utils/test-caps.d.ts +61 -0
  68. package/lib/utils/workspaceState.d.ts +21 -0
  69. package/package.json +21 -8
  70. package/public/status.html +278 -1
  71. package/lib/chunk-C8PTHxhX.mjs +0 -2
  72. package/lib/index.d.mts +0 -370
package/lib/types.d.ts ADDED
@@ -0,0 +1,181 @@
1
+ import type { Elysia, SingletonBase } from 'elysia';
2
+ import type { ReadableStream } from 'node:stream/web';
3
+ import type { Backend } from './index';
4
+ export type HumanReadableUnit = number | string;
5
+ type NormalizeKeys = 'max_body_size' | 'session_timeout' | 'max_size_bytes';
6
+ export type DeepNormalize<T> = T extends object ? {
7
+ [K in keyof T]: K extends NormalizeKeys ? number : DeepNormalize<T[K]>;
8
+ } : T;
9
+ export type HumanReadableServerConfig = {
10
+ id?: string;
11
+ name?: string;
12
+ port?: number;
13
+ log_level?: 'debug' | 'info' | 'warn' | 'error';
14
+ max_body_size?: HumanReadableUnit;
15
+ session_timeout?: HumanReadableUnit;
16
+ temp_file_dir?: string;
17
+ };
18
+ export type SessionCacheConfig = {
19
+ enabled?: boolean;
20
+ max_size_bytes?: HumanReadableUnit;
21
+ max_entries?: number;
22
+ };
23
+ export type RuntimeConfig = {
24
+ cache_dir?: string;
25
+ huggingface_token?: string;
26
+ session_cache?: SessionCacheConfig;
27
+ } & Record<string, any>;
28
+ export type GeneratorType = 'ggml-llm' | 'ggml-stt' | 'mlx-llm' | 'onnx-stt' | 'onnx-tts';
29
+ export type GeneratorConfig = {
30
+ type: GeneratorType;
31
+ } & Record<string, any>;
32
+ export type GlobalConfig = {
33
+ runtime?: RuntimeConfig;
34
+ openai_compat?: {
35
+ enabled?: boolean;
36
+ cors_allowed_origins?: string | string[];
37
+ };
38
+ anthropic_messages?: {
39
+ enabled?: boolean;
40
+ cors_allowed_origins?: string | string[];
41
+ };
42
+ functions?: {
43
+ enabled?: boolean;
44
+ /** Directory holding local function files, relative to the config file. */
45
+ dir?: string;
46
+ /** Allow calls on a server with no workspace binding (off by default). */
47
+ allow_unauthenticated?: boolean;
48
+ /** Default per-call deadline; ms number or duration string like "5m". */
49
+ default_timeout?: HumanReadableUnit;
50
+ /** Watch the functions dir and reload eagerly on change (off by default). */
51
+ hot_reload?: boolean;
52
+ /** Free-form table handed to functions as `context.config`. */
53
+ config?: Record<string, any>;
54
+ cors_allowed_origins?: string | string[];
55
+ };
56
+ } & Record<string, any>;
57
+ export type AutodiscoverConfig = {
58
+ udp: {
59
+ port?: number;
60
+ announcements: {
61
+ enabled: boolean;
62
+ interval?: number;
63
+ };
64
+ requests: {
65
+ enabled: boolean;
66
+ responseDelay?: number;
67
+ };
68
+ };
69
+ http: {
70
+ enabled: boolean;
71
+ path?: string;
72
+ cors?: boolean;
73
+ };
74
+ mdns?: {
75
+ enabled: boolean;
76
+ };
77
+ };
78
+ export type HumanReadableConfig = {
79
+ autodiscover?: AutodiscoverConfig | boolean;
80
+ server?: HumanReadableServerConfig;
81
+ generators?: GeneratorConfig[];
82
+ } & GlobalConfig;
83
+ export type ServerConfig = {
84
+ id: string;
85
+ name: string;
86
+ port: number;
87
+ log_level?: 'debug' | 'info' | 'warn' | 'error';
88
+ max_body_size: number;
89
+ session_timeout: number;
90
+ temp_file_dir: string;
91
+ };
92
+ export type Config = {
93
+ autodiscover: AutodiscoverConfig | null;
94
+ server: ServerConfig;
95
+ global: GlobalConfig;
96
+ generators: GeneratorConfig[];
97
+ };
98
+ export type GeneratorInfo = {
99
+ type: GeneratorType;
100
+ /** Performance score 0–100 from buttress-hardware-guardrails. */
101
+ score?: number;
102
+ /** Whether the host has an accelerator (GPU/Metal/etc) for this backend. */
103
+ hasGpu?: boolean;
104
+ /** Usable memory in bytes for this backend (GPU when present, else CPU). */
105
+ usableBytes?: number;
106
+ } & Record<string, any>;
107
+ export type ServerInfo = {
108
+ id: string;
109
+ name: string;
110
+ version: string;
111
+ address: string;
112
+ addresses?: string[];
113
+ port: number;
114
+ url: string;
115
+ generators: GeneratorInfo[];
116
+ authentication: {
117
+ required: boolean;
118
+ type: string;
119
+ /** Issuer key id (when type === 'workspace-jwt'). */
120
+ kid?: string;
121
+ /** True when buttress is paired with a workspace. */
122
+ bound?: boolean;
123
+ };
124
+ /** Workspace identity (only present when paired). */
125
+ workspace?: {
126
+ id: string;
127
+ name?: string;
128
+ };
129
+ /**
130
+ * Local function surface. Only a flag and a count: the whole ServerInfo is
131
+ * serialized into every UDP ANNOUNCE datagram, so the tool list itself must
132
+ * never go in here.
133
+ */
134
+ functions?: {
135
+ enabled: boolean;
136
+ count: number;
137
+ };
138
+ };
139
+ export type EventStream = {
140
+ event: string;
141
+ data: any;
142
+ };
143
+ export type StreamReaderEntry = {
144
+ reader: ReadableStreamDefaultReader<EventStream>;
145
+ peerId: string;
146
+ };
147
+ export type Session = {
148
+ streams: Map<string, ReadableStream<EventStream>>;
149
+ streamReaders: Map<string, StreamReaderEntry>;
150
+ generators: Set<string>;
151
+ fileManager: import('./utils/SessionFileManager').SessionFileManager;
152
+ initializedContexts: Set<string>;
153
+ timeout: ReturnType<typeof setTimeout> | null;
154
+ identity?: import('./utils/buttressAuth').VerifiedIdentity | null;
155
+ ready?: Promise<boolean>;
156
+ currentPeerId?: string | null;
157
+ };
158
+ export type State = {
159
+ serverInfo: ServerInfo;
160
+ backend: Backend;
161
+ config: Config;
162
+ sessions: Map<string, Session>;
163
+ workspaceState: import('./utils/workspaceState').WorkspaceState;
164
+ };
165
+ type ButtressSingleton = SingletonBase & {
166
+ store: State;
167
+ };
168
+ export type ButtressApp = Elysia<any, ButtressSingleton, any, any, any, any, any>;
169
+ export type MethodReturnType = string | object | number | boolean | null | ReadableStream<EventStream>;
170
+ export type ServiceContext = State & {
171
+ peerId: string;
172
+ session: Session;
173
+ };
174
+ export type ServiceMethodHandler<InputType = any, OutputType = any> = (ctx: ServiceContext, property: InputType) => OutputType | Promise<OutputType>;
175
+ export interface ServiceHandler {
176
+ [method: string]: ServiceMethodHandler;
177
+ }
178
+ export type Expand<T> = T extends object ? {
179
+ [K in keyof T]: Expand<T[K]>;
180
+ } : T;
181
+ export {};
@@ -0,0 +1,16 @@
1
+ import type { ReadableStream as NodeReadableStream } from 'node:stream/web';
2
+ type UploadData = Buffer | ArrayBuffer | ReadableStream | NodeReadableStream;
3
+ export declare class SessionFileManager {
4
+ sessionId: string;
5
+ private sessionDir;
6
+ private registry;
7
+ constructor(sessionId: string, tempDir: string);
8
+ register(filename: string, realPath: string, opts?: {
9
+ owned?: boolean;
10
+ ttl?: number;
11
+ }): void;
12
+ resolve(filename: string): string | null;
13
+ storeUpload(originalName: string, data: UploadData): Promise<string>;
14
+ destroy(): Promise<void>;
15
+ }
16
+ export {};
@@ -0,0 +1,25 @@
1
+ import type { WorkspaceBinding } from './workspaceState';
2
+ export declare const ACCESS_TOKEN_KIND = "ba";
3
+ export interface AccessTokenClaims {
4
+ k: typeof ACCESS_TOKEN_KIND;
5
+ w_id: string;
6
+ st: 'ws' | 'dev';
7
+ sid: string;
8
+ jti?: string;
9
+ iat?: number;
10
+ exp: number;
11
+ }
12
+ export interface VerifiedIdentity {
13
+ workspaceId: string;
14
+ subjectType: 'ws' | 'dev';
15
+ subjectId: string;
16
+ jti?: string;
17
+ exp: number;
18
+ }
19
+ /**
20
+ * Extract a buttress access token from a request. Prefers the Authorization
21
+ * Bearer header; falls back to ?access_token / ?token query params for clients
22
+ * (browser WebSocket, RN file transfer) that can't set headers.
23
+ */
24
+ export declare const extractAccessTokenFromRequest: (headers: Record<string, string | undefined> | undefined, query: Record<string, unknown> | undefined) => string | null;
25
+ export declare const verifyAccessToken: (token: string | null, binding: WorkspaceBinding | null) => Promise<VerifiedIdentity | null>;
@@ -0,0 +1,21 @@
1
+ import type { Config, AutodiscoverConfig, HumanReadableConfig } from '../types';
2
+ import type { Backend } from '../index';
3
+ export declare const deepMerge: (target?: Record<string, any>, source?: Record<string, any>) => Record<string, any>;
4
+ export declare const normalizeConfigInput: (input: any) => null | Record<string, any>;
5
+ export declare const mergeGeneratorConfig: (base: any, override: any) => Record<string, any>;
6
+ export declare const resolveDefaultConfig: (defaultConfigs: Config, backend: Backend, type: string, modelId: string | null) => Record<string, any> | null;
7
+ /**
8
+ * Normalize autodiscover config input.
9
+ * - undefined/false → null (disabled)
10
+ * - true → default config
11
+ * - object → merge with defaults
12
+ */
13
+ export declare const normalizeAutodiscoverConfig: (input: AutodiscoverConfig | boolean | undefined) => AutodiscoverConfig | null;
14
+ export declare const resolveSupportedGenerators: (config: Config, defaultTypes: string[]) => {
15
+ type: string;
16
+ }[];
17
+ /**
18
+ * Process raw user config (HumanReadableConfig) into Config.
19
+ * Handles: device defaults, value normalization (bytes/ms), structure splitting.
20
+ */
21
+ export declare const processConfig: (input: HumanReadableConfig | null) => Config;
@@ -0,0 +1,12 @@
1
+ import type { LoadedAuthFunction } from '../functions/types';
2
+ export type FunctionsAuthGuardOptions = {
3
+ allowUnauthenticated: boolean;
4
+ allowedOrigins?: string | string[];
5
+ /**
6
+ * Resolve the operator's `_auth` function; null when none exists. A
7
+ * rejection (present-but-broken auth file) fails closed: every call is
8
+ * rejected until the file loads again.
9
+ */
10
+ getCustomAuth?: () => Promise<LoadedAuthFunction | null>;
11
+ };
12
+ export declare const createFunctionsAuthGuard: ({ allowUnauthenticated, allowedOrigins, getCustomAuth, }: FunctionsAuthGuardOptions) => any;
@@ -0,0 +1 @@
1
+ export declare const buttressAuthGuard: any;
@@ -0,0 +1,6 @@
1
+ export declare const getLocalIpAddress: () => string | null;
2
+ export interface BroadcastInterface {
3
+ address: string;
4
+ broadcast: string;
5
+ }
6
+ export declare const getBroadcastInterfaces: () => BroadcastInterface[];
@@ -0,0 +1,2 @@
1
+ import type { ButtressApp } from '../types';
2
+ export declare const createRouter: (options?: any) => ButtressApp;
@@ -0,0 +1,2 @@
1
+ export declare const deserialize: (input: string) => any;
2
+ export declare const serialize: (output: any) => string;
@@ -0,0 +1,4 @@
1
+ import type { GeneratorInfo, GeneratorType } from '../types';
2
+ export declare const enrichGeneratorsWithCaps: (generators: {
3
+ type: GeneratorType;
4
+ }[]) => Promise<GeneratorInfo[]>;
@@ -0,0 +1,33 @@
1
+ import type { Session } from '../types';
2
+ type AnyRecord = Record<string, unknown>;
3
+ /**
4
+ * Extract a session id from the `X-Buttress-Sess-Id` request header.
5
+ * Query-param fallback is intentionally not supported: session ids would
6
+ * otherwise show up in proxy access logs and browser history, and there is
7
+ * no transport in our supported clients that can't set custom headers.
8
+ */
9
+ export declare const extractSessionIdFromRequest: (headers: Record<string, string | undefined> | undefined) => string | null;
10
+ /**
11
+ * Elysia `beforeHandle` guard for HTTP routes that operate on a buttress
12
+ * session (file upload/download). Verifies:
13
+ *
14
+ * 1. A session id is supplied via the `X-Buttress-Sess-Id` header.
15
+ * 2. The session exists in the in-memory map (i.e. `init` has been issued
16
+ * over WS for this client).
17
+ * 3. When the server is bound to a workspace, the request's access token
18
+ * identity matches the identity recorded on the session — preventing
19
+ * one device in a workspace from acting on another device's session.
20
+ *
21
+ * Handlers can re-extract the resolved session via `resolveGuardedSession`.
22
+ */
23
+ export declare const buttressSessionGuard: any;
24
+ /**
25
+ * Re-resolve the session that `buttressSessionGuard` validated. Returns null
26
+ * only if called outside the guard's protection (programming error). Handlers
27
+ * should treat null as 500 / unreachable.
28
+ */
29
+ export declare const resolveGuardedSession: (store: AnyRecord, headers: Record<string, string | undefined>) => {
30
+ sessionId: string;
31
+ session: Session;
32
+ } | null;
33
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Display model capabilities in a Markdown table format
3
+ * @param {Object} options - Display options
4
+ * @param {Array<string>} options.modelIds - Array of model repository IDs
5
+ * @param {Object|null} options.defaultConfig - Default configuration from TOML
6
+ * @returns {Promise<void>}
7
+ */
8
+ export declare function showModelsTable({ modelIds, defaultConfig, }?: {
9
+ modelIds?: string[];
10
+ defaultConfig?: any;
11
+ }): Promise<void>;
12
+ /**
13
+ * Test ggml-llm capabilities for a backend type with optional model configuration
14
+ * @param {Object} options - Test options
15
+ * @param {string|null} options.modelId - Model repository ID
16
+ * @param {Object|null} options.defaultConfig - Default configuration from TOML
17
+ * @returns {Promise<void>}
18
+ */
19
+ export declare function testGgmlLlmCapabilities({ modelId, defaultConfig, }?: {
20
+ modelId?: string | null;
21
+ defaultConfig?: any;
22
+ }): Promise<void>;
23
+ /**
24
+ * Display STT model capabilities in a Markdown table format
25
+ * @param {Object} options - Display options
26
+ * @param {Array<string>} options.modelIds - Array of model IDs (format: repo_id:filename)
27
+ * @param {Object|null} options.defaultConfig - Default configuration from TOML
28
+ * @returns {Promise<void>}
29
+ */
30
+ export declare function showSttModelsTable({ modelIds, defaultConfig, }?: {
31
+ modelIds?: string[];
32
+ defaultConfig?: any;
33
+ }): Promise<void>;
34
+ /**
35
+ * Test ggml-stt capabilities for a backend type with optional model configuration
36
+ * @param {Object} options - Test options
37
+ * @param {string|null} options.modelId - Model ID (format: repo_id:filename)
38
+ * @param {Object|null} options.defaultConfig - Default configuration from TOML
39
+ * @returns {Promise<void>}
40
+ */
41
+ /**
42
+ * Display MLX model capabilities in a Markdown table format.
43
+ *
44
+ * MLX getCapabilities is a platform/python check (no per-model GGUF metadata),
45
+ * so the table shows hardware fit based on unified memory and model repo IDs.
46
+ */
47
+ export declare function showMlxModelsTable({ modelIds, defaultConfig, }?: {
48
+ modelIds?: string[];
49
+ defaultConfig?: any;
50
+ }): Promise<void>;
51
+ /**
52
+ * Test MLX LLM capabilities for a single model with detailed output
53
+ */
54
+ export declare function testMlxLlmCapabilities({ modelId, defaultConfig, }?: {
55
+ modelId?: string | null;
56
+ defaultConfig?: any;
57
+ }): Promise<void>;
58
+ export declare function testGgmlSttCapabilities({ modelId, defaultConfig, }?: {
59
+ modelId?: string | null;
60
+ defaultConfig?: any;
61
+ }): Promise<void>;
@@ -0,0 +1,21 @@
1
+ export interface WorkspaceBinding {
2
+ id: string;
3
+ name?: string;
4
+ serverId: string;
5
+ issuerPublicKey: string;
6
+ kid: string;
7
+ boundAt: string;
8
+ }
9
+ export interface ServerKeyPair {
10
+ publicKeySpki: string;
11
+ privateKeyPkcs8: string;
12
+ kid: string;
13
+ }
14
+ export interface WorkspaceState {
15
+ workspace: WorkspaceBinding | null;
16
+ serverKeyPair: ServerKeyPair | null;
17
+ }
18
+ export declare const resolveStateDir: () => string;
19
+ export declare const resolveStatePath: () => string;
20
+ export declare const loadWorkspaceState: () => WorkspaceState;
21
+ export declare const saveWorkspaceState: (state: WorkspaceState) => Promise<void>;
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@fugood/buttress-server",
3
- "version": "2.25.0-beta.9",
3
+ "version": "2.25.1-beta.0",
4
4
  "main": "lib/index.mjs",
5
- "types": "lib/index.d.mts",
5
+ "types": "lib/index.d.ts",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "bricks-buttress": "./bin/bricks-buttress"
@@ -15,7 +15,7 @@
15
15
  ],
16
16
  "scripts": {
17
17
  "typecheck": "tsc --noEmit",
18
- "build": "tsdown -c rolldown.config.js",
18
+ "build": "tsdown -c rolldown.config.js --config-loader native && tsc --noCheck --emitDeclarationOnly",
19
19
  "prepublish": "bun run build",
20
20
  "dev": "bun src/index.ts",
21
21
  "start": "bun lib/index.mjs",
@@ -30,20 +30,33 @@
30
30
  "dependencies": {
31
31
  "@elysiajs/cors": "^1.1.1",
32
32
  "@elysiajs/node": "^1.4.2",
33
- "@fugood/llama.node": "^1.7.0-rc.11",
34
- "@fugood/whisper.node": "^1.0.18",
33
+ "@fugood/llama.node": "1.7.11",
34
+ "@fugood/whisper.node": "^1.1.1",
35
35
  "@huggingface/gguf": "^0.3.2",
36
36
  "@iarna/toml": "^3.0.0",
37
+ "@modelcontextprotocol/sdk": "^1.15.0",
37
38
  "bytes": "^3.1.0",
39
+ "chroma-js": "^2.1.2",
38
40
  "elysia": "^1.4.19",
39
41
  "jose": "^5.9.6",
42
+ "json5": "^2.0.1",
43
+ "lodash": "^4.17.4",
44
+ "mathjs": "^12.2.0",
45
+ "md5": "^2.2.1",
46
+ "moment": "^2.22.2",
40
47
  "ms": "^2.1.1",
48
+ "nanoid": "^3.1.4",
41
49
  "node-machine-id": "^1.1.12",
50
+ "onnxruntime-node": "1.24.3",
51
+ "oxc-transform": "^0.105.0",
52
+ "qs": "^6.15.0",
53
+ "sharp": "^0.34.1",
54
+ "voca": "^1.4.1",
42
55
  "zod": "^3.25.76"
43
56
  },
44
57
  "devDependencies": {
45
- "tsdown": "^0.20.1",
46
- "typescript": "^5.9.3"
58
+ "tsdown": "^0.22.4",
59
+ "typescript": "^7.0.2"
47
60
  },
48
- "gitHead": "9274cc21f1e882d26f80e45a972d814d648c861a"
61
+ "gitHead": "95e3bdbcf4d433d89c5ef7ddc67f046458bfc240"
49
62
  }