@heysalad/cheri-cli 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
- # Cheri CLI
1
+ # cheri-cli
2
2
 
3
- AI-powered cloud IDE by [HeySalad](https://heysalad.app). Like Claude Code, but for cloud workspaces.
3
+ CLI for [Cheri](https://cheri.heysalad.app) the AI-powered cloud IDE that never forgets.
4
+
5
+ Manage workspaces, track API usage, and access your AI memory from the terminal.
4
6
 
5
7
  ## Install
6
8
 
@@ -8,53 +10,79 @@ AI-powered cloud IDE by [HeySalad](https://heysalad.app). Like Claude Code, but
8
10
  npm install -g @heysalad/cheri-cli
9
11
  ```
10
12
 
11
- ## Usage
13
+ Requires Node.js 18+.
14
+
15
+ ## Quick Start
12
16
 
13
17
  ```bash
14
- # Login to your Cheri account
18
+ # Authenticate with your Cheri account
15
19
  cheri login
16
20
 
21
+ # Launch a cloud workspace
22
+ cheri workspace launch owner/my-repo
23
+
17
24
  # Check account status
18
25
  cheri status
19
26
 
20
- # Launch a cloud workspace
21
- cheri workspace launch owner/repo
22
-
23
- # List your workspaces
24
- cheri workspace list
27
+ # View API usage and rate limits
28
+ cheri usage
29
+ ```
25
30
 
26
- # Stop a workspace
27
- cheri workspace stop
31
+ ## Commands
32
+
33
+ | Command | Description |
34
+ |---|---|
35
+ | `cheri login` | Authenticate with GitHub |
36
+ | `cheri status` | Show account and workspace status |
37
+ | `cheri usage` | Show API usage and rate limit status |
38
+ | `cheri workspace launch <repo>` | Launch a new cloud workspace |
39
+ | `cheri workspace list` | List all workspaces |
40
+ | `cheri workspace stop <id>` | Stop a running workspace |
41
+ | `cheri workspace status <id>` | Get workspace status |
42
+ | `cheri memory show` | Show current memory entries |
43
+ | `cheri memory add <text>` | Add a memory entry |
44
+ | `cheri memory clear` | Clear all memory |
45
+ | `cheri memory export` | Export memory to JSON |
46
+ | `cheri config list` | Show all configuration |
47
+ | `cheri config get <key>` | Get a config value |
48
+ | `cheri config set <key> <value>` | Set a config value |
49
+ | `cheri init` | Initialize a project |
50
+
51
+ ## Interactive REPL
52
+
53
+ Run `cheri` with no arguments to enter the interactive REPL:
28
54
 
29
- # Initialize AI project config
30
- cheri init
55
+ ```
56
+ $ cheri
57
+ 🍒 cheri > help
58
+ 🍒 cheri > workspace list
59
+ 🍒 cheri > usage
60
+ 🍒 cheri > exit
61
+ ```
31
62
 
32
- # Manage persistent memory
33
- cheri memory show
34
- cheri memory add "Always use TypeScript strict mode"
35
- cheri memory clear
63
+ ## Rate Limits
36
64
 
37
- # View/update configuration
38
- cheri config list
39
- cheri config set apiUrl https://cheri.heysalad.app
40
- ```
65
+ | Plan | Limit |
66
+ |---|---|
67
+ | Free | 100 requests/hour |
68
+ | Pro | 1,000 requests/hour |
41
69
 
42
- ## How it works
70
+ Use `cheri usage` to check your current rate limit status.
43
71
 
44
- 1. **`cheri login`** opens your browser for GitHub OAuth, then you paste your API token
45
- 2. **`cheri workspace launch`** spins up a cloud workspace with code-server (VS Code in browser)
46
- 3. **`cheri memory`** stores persistent context that follows you across sessions
47
- 4. **`cheri init`** creates a local `.ai/` directory with project constitution files
72
+ ## Configuration
48
73
 
49
- ## Requirements
74
+ Config is stored in `~/.cheri/`. Set the API URL if self-hosting:
50
75
 
51
- - Node.js >= 18
76
+ ```bash
77
+ cheri config set apiUrl https://your-instance.example.com
78
+ ```
52
79
 
53
80
  ## Links
54
81
 
55
82
  - [Cheri Cloud IDE](https://cheri.heysalad.app)
56
- - [GitHub](https://github.com/Hey-Salad/cheri-cli)
83
+ - [Dashboard](https://cheri.heysalad.app/dashboard)
84
+ - [GitHub](https://github.com/chilu18/cloud-ide)
57
85
 
58
86
  ## License
59
87
 
60
- MIT - HeySalad
88
+ MIT
package/bin/cheri.js CHANGED
@@ -7,13 +7,13 @@ import { registerStatusCommand } from "../src/commands/status.js";
7
7
  import { registerMemoryCommand } from "../src/commands/memory.js";
8
8
  import { registerConfigCommand } from "../src/commands/config.js";
9
9
  import { registerWorkspaceCommand } from "../src/commands/workspace.js";
10
- import { registerChatCommand } from "../src/commands/chat.js";
10
+ import { registerUsageCommand } from "../src/commands/usage.js";
11
11
  import { registerAgentCommand } from "../src/commands/agent.js";
12
12
 
13
13
  program
14
14
  .name("cheri")
15
15
  .description("Cheri CLI - AI-powered cloud IDE by HeySalad")
16
- .version("0.2.0");
16
+ .version("0.1.0");
17
17
 
18
18
  registerLoginCommand(program);
19
19
  registerInitCommand(program);
@@ -21,7 +21,7 @@ registerStatusCommand(program);
21
21
  registerMemoryCommand(program);
22
22
  registerConfigCommand(program);
23
23
  registerWorkspaceCommand(program);
24
- registerChatCommand(program);
24
+ registerUsageCommand(program);
25
25
  registerAgentCommand(program);
26
26
 
27
27
  // If no args, launch interactive command REPL
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heysalad/cheri-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Cheri CLI - AI-powered cloud IDE by HeySalad. Like Claude Code, but for cloud workspaces.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,15 +8,14 @@
8
8
  },
9
9
  "files": [
10
10
  "bin/",
11
- "src/",
12
- "README.md"
11
+ "src/"
13
12
  ],
14
13
  "scripts": {
15
14
  "start": "node bin/cheri.js",
16
15
  "dev": "node bin/cheri.js",
17
- "release:patch": "npm version patch && npm publish --access public && git push && git push --tags",
18
- "release:minor": "npm version minor && npm publish --access public && git push && git push --tags",
19
- "release:major": "npm version major && npm publish --access public && git push && git push --tags"
16
+ "release:patch": "npm version patch && npm publish && git push && git push --tags",
17
+ "release:minor": "npm version minor && npm publish && git push && git push --tags",
18
+ "release:major": "npm version major && npm publish && git push && git push --tags"
20
19
  },
21
20
  "keywords": [
22
21
  "cloud-ide",
@@ -29,23 +28,18 @@
29
28
  ],
30
29
  "repository": {
31
30
  "type": "git",
32
- "url": "https://github.com/Hey-Salad/cheri-cli.git"
31
+ "url": "https://github.com/chilu18/cloud-ide.git",
32
+ "directory": "cli"
33
33
  },
34
- "homepage": "https://cheri.heysalad.app",
35
34
  "author": "HeySalad",
36
35
  "license": "MIT",
37
36
  "engines": {
38
37
  "node": ">=18"
39
38
  },
40
39
  "dependencies": {
41
- "@anthropic-ai/sdk": "^0.74.0",
42
- "@google/generative-ai": "^0.24.1",
43
40
  "chalk": "^5.3.0",
44
41
  "commander": "^12.1.0",
45
42
  "inquirer": "^9.2.23",
46
- "marked": "^15.0.12",
47
- "marked-terminal": "^7.3.0",
48
- "openai": "^6.22.0",
49
43
  "ora": "^8.0.1"
50
44
  }
51
45
  }
@@ -1,6 +1,4 @@
1
1
  import { apiClient } from "../lib/api-client.js";
2
- import { getConfigValue, setConfigValue } from "../lib/config-store.js";
3
- import { createProvider } from "../lib/providers/index.js";
4
2
  import { log } from "../lib/logger.js";
5
3
  import chalk from "chalk";
6
4
 
@@ -8,96 +6,121 @@ const SYSTEM_PROMPT = `You are Cheri Agent, an AI assistant for the Cheri cloud
8
6
 
9
7
  const TOOLS = [
10
8
  {
11
- name: "get_account_info",
12
- description: "Get the current user's account information",
13
- parameters: { type: "object", properties: {}, required: [] },
9
+ type: "function",
10
+ function: {
11
+ name: "get_account_info",
12
+ description: "Get the current user's account information",
13
+ parameters: { type: "object", properties: {}, required: [] },
14
+ },
14
15
  },
15
16
  {
16
- name: "list_workspaces",
17
- description: "List all cloud workspaces for the current user",
18
- parameters: { type: "object", properties: {}, required: [] },
17
+ type: "function",
18
+ function: {
19
+ name: "list_workspaces",
20
+ description: "List all cloud workspaces for the current user",
21
+ parameters: { type: "object", properties: {}, required: [] },
22
+ },
19
23
  },
20
24
  {
21
- name: "create_workspace",
22
- description: "Launch a new cloud workspace for a GitHub repository",
23
- parameters: {
24
- type: "object",
25
- properties: {
26
- repo: { type: "string", description: "GitHub repo in owner/name format" },
25
+ type: "function",
26
+ function: {
27
+ name: "create_workspace",
28
+ description: "Launch a new cloud workspace for a GitHub repository",
29
+ parameters: {
30
+ type: "object",
31
+ properties: { repo: { type: "string", description: "GitHub repo in owner/name format" } },
32
+ required: ["repo"],
27
33
  },
28
- required: ["repo"],
29
34
  },
30
35
  },
31
36
  {
32
- name: "stop_workspace",
33
- description: "Stop and delete a running workspace",
34
- parameters: {
35
- type: "object",
36
- properties: {
37
- id: { type: "string", description: "Workspace ID to stop" },
37
+ type: "function",
38
+ function: {
39
+ name: "stop_workspace",
40
+ description: "Stop and delete a running workspace",
41
+ parameters: {
42
+ type: "object",
43
+ properties: { id: { type: "string", description: "Workspace ID to stop" } },
44
+ required: ["id"],
38
45
  },
39
- required: ["id"],
40
46
  },
41
47
  },
42
48
  {
43
- name: "get_workspace_status",
44
- description: "Get the status of a specific workspace",
45
- parameters: {
46
- type: "object",
47
- properties: {
48
- id: { type: "string", description: "Workspace ID" },
49
+ type: "function",
50
+ function: {
51
+ name: "get_workspace_status",
52
+ description: "Get the status of a specific workspace",
53
+ parameters: {
54
+ type: "object",
55
+ properties: { id: { type: "string", description: "Workspace ID" } },
56
+ required: ["id"],
49
57
  },
50
- required: ["id"],
51
58
  },
52
59
  },
53
60
  {
54
- name: "get_memory",
55
- description: "Retrieve all stored memory entries",
56
- parameters: { type: "object", properties: {}, required: [] },
61
+ type: "function",
62
+ function: {
63
+ name: "get_memory",
64
+ description: "Retrieve all stored memory entries",
65
+ parameters: { type: "object", properties: {}, required: [] },
66
+ },
57
67
  },
58
68
  {
59
- name: "add_memory",
60
- description: "Add a new memory entry for the user",
61
- parameters: {
62
- type: "object",
63
- properties: {
64
- content: { type: "string", description: "Memory content to store" },
65
- category: { type: "string", description: "Optional category (defaults to 'general')" },
69
+ type: "function",
70
+ function: {
71
+ name: "add_memory",
72
+ description: "Add a new memory entry for the user",
73
+ parameters: {
74
+ type: "object",
75
+ properties: {
76
+ content: { type: "string", description: "Memory content to store" },
77
+ category: { type: "string", description: "Optional category (defaults to 'general')" },
78
+ },
79
+ required: ["content"],
66
80
  },
67
- required: ["content"],
68
81
  },
69
82
  },
70
83
  {
71
- name: "clear_memory",
72
- description: "Clear all stored memory entries",
73
- parameters: { type: "object", properties: {}, required: [] },
84
+ type: "function",
85
+ function: {
86
+ name: "clear_memory",
87
+ description: "Clear all stored memory entries",
88
+ parameters: { type: "object", properties: {}, required: [] },
89
+ },
74
90
  },
75
91
  {
76
- name: "get_usage",
77
- description: "Get the user's API usage and rate limit statistics",
78
- parameters: { type: "object", properties: {}, required: [] },
92
+ type: "function",
93
+ function: {
94
+ name: "get_usage",
95
+ description: "Get the user's API usage and rate limit statistics",
96
+ parameters: { type: "object", properties: {}, required: [] },
97
+ },
79
98
  },
80
99
  {
81
- name: "get_config",
82
- description: "Get a configuration value by key (dot notation supported)",
83
- parameters: {
84
- type: "object",
85
- properties: {
86
- key: { type: "string", description: "Config key, e.g. 'ai.provider'" },
100
+ type: "function",
101
+ function: {
102
+ name: "get_config",
103
+ description: "Get a configuration value by key (dot notation supported)",
104
+ parameters: {
105
+ type: "object",
106
+ properties: { key: { type: "string", description: "Config key, e.g. 'ai.provider'" } },
107
+ required: ["key"],
87
108
  },
88
- required: ["key"],
89
109
  },
90
110
  },
91
111
  {
92
- name: "set_config",
93
- description: "Set a configuration value",
94
- parameters: {
95
- type: "object",
96
- properties: {
97
- key: { type: "string", description: "Config key" },
98
- value: { type: "string", description: "Value to set" },
112
+ type: "function",
113
+ function: {
114
+ name: "set_config",
115
+ description: "Set a configuration value",
116
+ parameters: {
117
+ type: "object",
118
+ properties: {
119
+ key: { type: "string", description: "Config key" },
120
+ value: { type: "string", description: "Value to set" },
121
+ },
122
+ required: ["key", "value"],
99
123
  },
100
- required: ["key", "value"],
101
124
  },
102
125
  },
103
126
  ];
@@ -123,11 +146,15 @@ async function executeTool(name, args) {
123
146
  return await apiClient.clearMemory();
124
147
  case "get_usage":
125
148
  return await apiClient.getUsage();
126
- case "get_config":
149
+ case "get_config": {
150
+ const { getConfigValue } = await import("../lib/config-store.js");
127
151
  return { key: args.key, value: getConfigValue(args.key) };
128
- case "set_config":
152
+ }
153
+ case "set_config": {
154
+ const { setConfigValue } = await import("../lib/config-store.js");
129
155
  setConfigValue(args.key, args.value);
130
156
  return { key: args.key, value: args.value, status: "updated" };
157
+ }
131
158
  default:
132
159
  return { error: `Unknown tool: ${name}` };
133
160
  }
@@ -136,72 +163,101 @@ async function executeTool(name, args) {
136
163
  }
137
164
  }
138
165
 
139
- export async function runAgent(userRequest) {
140
- const providerName = getConfigValue("agent.provider") || getConfigValue("ai.provider") || "anthropic";
141
- const model = getConfigValue("agent.model") || undefined;
166
+ // Parse SSE stream from the cloud proxy
167
+ async function* parseSSEStream(response) {
168
+ const reader = response.body.getReader();
169
+ const decoder = new TextDecoder();
170
+ let buffer = "";
171
+
172
+ try {
173
+ while (true) {
174
+ const { done, value } = await reader.read();
175
+ if (done) break;
176
+
177
+ buffer += decoder.decode(value, { stream: true });
178
+ const lines = buffer.split("\n");
179
+ buffer = lines.pop() || "";
142
180
 
143
- const provider = await createProvider({ provider: providerName, model });
181
+ for (const line of lines) {
182
+ if (line.startsWith("data: ")) {
183
+ const data = line.slice(6).trim();
184
+ if (data === "[DONE]") return;
185
+ try {
186
+ yield JSON.parse(data);
187
+ } catch {}
188
+ }
189
+ }
190
+ }
191
+ } finally {
192
+ reader.releaseLock();
193
+ }
194
+ }
144
195
 
196
+ export async function runAgent(userRequest) {
145
197
  const messages = [
198
+ { role: "system", content: SYSTEM_PROMPT },
146
199
  { role: "user", content: userRequest },
147
200
  ];
148
201
 
149
202
  const MAX_ITERATIONS = 10;
150
203
 
151
204
  for (let i = 0; i < MAX_ITERATIONS; i++) {
205
+ const response = await apiClient.chatStream(messages, TOOLS);
206
+
152
207
  let fullText = "";
153
- const toolCalls = [];
154
-
155
- for await (const event of provider.chat(messages, TOOLS, { systemPrompt: SYSTEM_PROMPT })) {
156
- switch (event.type) {
157
- case "text":
158
- process.stdout.write(event.content);
159
- fullText += event.content;
160
- break;
161
-
162
- case "tool_use_start":
163
- toolCalls.push({ id: event.id, name: event.name, input: {} });
164
- break;
165
-
166
- case "tool_input_delta":
167
- // accumulated by provider, final input comes in tool_use_end
168
- break;
169
-
170
- case "tool_use_end":
171
- if (toolCalls.length > 0) {
172
- toolCalls[toolCalls.length - 1].input = event.input;
173
- }
174
- break;
208
+ const toolCalls = {};
209
+
210
+ for await (const chunk of parseSSEStream(response)) {
211
+ const delta = chunk.choices?.[0]?.delta;
212
+ const finishReason = chunk.choices?.[0]?.finish_reason;
213
+
214
+ if (delta?.content) {
215
+ process.stdout.write(delta.content);
216
+ fullText += delta.content;
217
+ }
175
218
 
176
- case "done":
177
- break;
219
+ if (delta?.tool_calls) {
220
+ for (const tc of delta.tool_calls) {
221
+ const idx = tc.index;
222
+ if (!toolCalls[idx]) {
223
+ toolCalls[idx] = { id: tc.id || "", name: "", arguments: "" };
224
+ }
225
+ if (tc.id) toolCalls[idx].id = tc.id;
226
+ if (tc.function?.name) toolCalls[idx].name = tc.function.name;
227
+ if (tc.function?.arguments) toolCalls[idx].arguments += tc.function.arguments;
228
+ }
178
229
  }
230
+
231
+ if (finishReason) break;
179
232
  }
180
233
 
234
+ const toolCallList = Object.values(toolCalls);
235
+
181
236
  // No tool calls — final text response, done
182
- if (toolCalls.length === 0) {
237
+ if (toolCallList.length === 0) {
183
238
  if (fullText) process.stdout.write("\n");
184
239
  return;
185
240
  }
186
241
 
187
242
  if (fullText) process.stdout.write("\n");
188
243
 
189
- // Build assistant message in Anthropic content-block format
190
- const assistantContent = [];
191
- if (fullText) {
192
- assistantContent.push({ type: "text", text: fullText });
193
- }
194
- for (const tc of toolCalls) {
195
- assistantContent.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.input });
196
- }
197
- messages.push({ role: "assistant", content: assistantContent });
244
+ // Build assistant message with tool calls
245
+ const assistantMsg = { role: "assistant", content: fullText || null };
246
+ assistantMsg.tool_calls = toolCallList.map((tc) => ({
247
+ id: tc.id,
248
+ type: "function",
249
+ function: { name: tc.name, arguments: tc.arguments },
250
+ }));
251
+ messages.push(assistantMsg);
252
+
253
+ // Execute each tool and add results
254
+ for (const tc of toolCallList) {
255
+ let input = {};
256
+ try { input = JSON.parse(tc.arguments); } catch {}
198
257
 
199
- // Execute each tool call and build tool results
200
- const toolResults = [];
201
- for (const tc of toolCalls) {
202
- log.info(`Calling ${chalk.cyan(tc.name)}${Object.keys(tc.input).length ? chalk.dim(" " + JSON.stringify(tc.input)) : ""}`);
258
+ log.info(`Calling ${chalk.cyan(tc.name)}${Object.keys(input).length ? chalk.dim(" " + JSON.stringify(input)) : ""}`);
203
259
 
204
- const result = await executeTool(tc.name, tc.input);
260
+ const result = await executeTool(tc.name, input);
205
261
 
206
262
  if (result.error) {
207
263
  log.error(result.error);
@@ -209,14 +265,12 @@ export async function runAgent(userRequest) {
209
265
  log.success(tc.name);
210
266
  }
211
267
 
212
- toolResults.push({
213
- type: "tool_result",
214
- tool_use_id: tc.id,
268
+ messages.push({
269
+ role: "tool",
270
+ tool_call_id: tc.id,
215
271
  content: JSON.stringify(result),
216
272
  });
217
273
  }
218
-
219
- messages.push({ role: "user", content: toolResults });
220
274
  }
221
275
 
222
276
  log.warn("Agent reached maximum iterations (10). Stopping.");
@@ -53,21 +53,13 @@ export async function showMemory(options = {}) {
53
53
  }
54
54
 
55
55
  export async function addMemory(content, category = "general") {
56
- try {
57
- const { entry, count } = await apiClient.addMemory(content, category);
58
- log.success(`Memory saved (${count} total). Category: ${chalk.cyan(entry.category)}`);
59
- } catch (err) {
60
- throw err;
61
- }
56
+ const { entry, count } = await apiClient.addMemory(content, category);
57
+ log.success(`Memory saved (${count} total). Category: ${chalk.cyan(entry.category)}`);
62
58
  }
63
59
 
64
60
  export async function clearMemory() {
65
- try {
66
- await apiClient.clearMemory();
67
- log.success("All memories cleared.");
68
- } catch (err) {
69
- throw err;
70
- }
61
+ await apiClient.clearMemory();
62
+ log.success("All memories cleared.");
71
63
  }
72
64
 
73
65
  export async function exportMemory(options = {}) {
@@ -0,0 +1,64 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { apiClient } from "../lib/api-client.js";
4
+ import { log } from "../lib/logger.js";
5
+
6
+ export async function showUsage() {
7
+ log.blank();
8
+ log.brand("Usage");
9
+
10
+ const spinner = ora("Fetching usage data...").start();
11
+
12
+ try {
13
+ const data = await apiClient.getUsage();
14
+ spinner.stop();
15
+
16
+ // Rate limit
17
+ log.header("Rate Limit");
18
+ log.keyValue("Plan", data.plan === "pro" ? chalk.green("Pro") : "Free");
19
+ log.keyValue("Limit", `${data.rateLimit.limit} requests/hour`);
20
+ const remaining = data.rateLimit.remaining;
21
+ const limit = data.rateLimit.limit;
22
+ const remainColor = remaining > limit * 0.5 ? chalk.green : remaining > limit * 0.1 ? chalk.yellow : chalk.red;
23
+ log.keyValue("Remaining", remainColor(`${remaining}`));
24
+ log.keyValue("Resets at", data.rateLimit.resetsAt);
25
+
26
+ // Today's usage
27
+ log.header("Today");
28
+ log.keyValue("Requests", `${data.usage.today.requests}`);
29
+ const endpoints = data.usage.today.endpoints || {};
30
+ if (Object.keys(endpoints).length > 0) {
31
+ for (const [ep, count] of Object.entries(endpoints)) {
32
+ console.log(` ${chalk.dim(ep)} ${chalk.cyan(count)}`);
33
+ }
34
+ }
35
+
36
+ // Summary
37
+ log.header("Summary");
38
+ log.keyValue("Last 7 days", `${data.usage.last7d.requests} requests`);
39
+ log.keyValue("Last 30 days", `${data.usage.last30d.requests} requests`);
40
+ log.keyValue("All time", `${data.summary.totalRequests} requests`);
41
+ if (data.summary.memberSince) {
42
+ log.keyValue("Member since", new Date(data.summary.memberSince).toLocaleDateString());
43
+ }
44
+ } catch (err) {
45
+ spinner.stop();
46
+ log.error(err.message);
47
+ }
48
+
49
+ log.blank();
50
+ }
51
+
52
+ export function registerUsageCommand(program) {
53
+ program
54
+ .command("usage")
55
+ .description("Show API usage and rate limit status")
56
+ .action(async () => {
57
+ try {
58
+ await showUsage();
59
+ } catch (err) {
60
+ log.error(err.message);
61
+ process.exit(1);
62
+ }
63
+ });
64
+ }