@cremini/skillpack 1.0.9 → 1.1.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 (40) hide show
  1. package/README.md +4 -4
  2. package/dist/cli.js +4 -0
  3. package/package.json +4 -3
  4. package/runtime/README.md +11 -1
  5. package/runtime/server/dist/adapters/markdown.js +74 -0
  6. package/runtime/server/dist/adapters/markdown.js.map +1 -0
  7. package/runtime/server/dist/adapters/slack.js +369 -0
  8. package/runtime/server/dist/adapters/slack.js.map +1 -0
  9. package/runtime/server/dist/adapters/telegram.js +199 -0
  10. package/runtime/server/dist/adapters/telegram.js.map +1 -0
  11. package/runtime/server/dist/adapters/types.js +2 -0
  12. package/runtime/server/dist/adapters/types.js.map +1 -0
  13. package/runtime/server/dist/adapters/web.js +201 -0
  14. package/runtime/server/dist/adapters/web.js.map +1 -0
  15. package/runtime/server/dist/agent.js +223 -0
  16. package/runtime/server/dist/agent.js.map +1 -0
  17. package/runtime/server/dist/config.js +79 -0
  18. package/runtime/server/dist/config.js.map +1 -0
  19. package/runtime/server/dist/index.js +146 -0
  20. package/runtime/server/dist/index.js.map +1 -0
  21. package/runtime/server/dist/lifecycle.js +85 -0
  22. package/runtime/server/dist/lifecycle.js.map +1 -0
  23. package/runtime/server/dist/memory.js +195 -0
  24. package/runtime/server/dist/memory.js.map +1 -0
  25. package/runtime/server/package-lock.json +4028 -244
  26. package/runtime/server/package.json +13 -3
  27. package/runtime/start.bat +40 -4
  28. package/runtime/start.sh +30 -2
  29. package/runtime/web/index.html +145 -18
  30. package/runtime/web/js/api-key-dialog.js +153 -0
  31. package/runtime/web/js/api.js +25 -0
  32. package/runtime/web/js/chat-apps-dialog.js +194 -0
  33. package/runtime/web/{app.js → js/chat.js} +112 -193
  34. package/runtime/web/js/config.js +16 -0
  35. package/runtime/web/js/main.js +56 -0
  36. package/runtime/web/js/settings.js +205 -0
  37. package/runtime/web/styles.css +311 -10
  38. package/runtime/server/chat-proxy.js +0 -229
  39. package/runtime/server/index.js +0 -63
  40. package/runtime/server/routes.js +0 -104
@@ -1,69 +1,47 @@
1
- const API_BASE = "";
2
- let chatHistory = [];
1
+ import { state } from "./config.js";
3
2
 
4
- // Initialize
5
- async function init() {
6
- await loadConfig();
7
- setupEventListeners();
8
- }
9
-
10
- async function loadConfig() {
11
- try {
12
- const res = await fetch(API_BASE + "/api/config");
13
- const config = await res.json();
14
-
15
- document.getElementById("pack-name").textContent = config.name;
16
- document.getElementById("pack-desc").textContent = config.description;
17
- document.title = config.name;
18
-
19
- // Skills
20
- const skillsList = document.getElementById("skills-list");
21
- skillsList.innerHTML = config.skills
22
- .map(
23
- (s) =>
24
- `<li><div class="skill-name">${s.name}</div><div class="skill-desc">${s.description}</div></li>`,
25
- )
26
- .join("");
27
-
28
- // Pre-fill when there is exactly one prompt
29
- if (config.prompts && config.prompts.length === 1) {
30
- const input = document.getElementById("user-input");
31
- input.value = config.prompts[0];
32
- input.style.height = "auto";
33
- input.style.height = Math.min(input.scrollHeight, 120) + "px";
34
- }
3
+ export const chatHistory = [];
4
+ let ws = null;
5
+ let currentAssistantMsg = null;
35
6
 
36
- // API key status and provider
37
- const keyStatus = document.getElementById("key-status");
38
- if (config.hasApiKey) {
39
- keyStatus.textContent = "API key configured";
40
- keyStatus.className = "status-text success";
41
- }
7
+ export function initChat() {
8
+ // Send button
9
+ document.getElementById("send-btn").addEventListener("click", sendMessage);
42
10
 
43
- const providerSelect = document.getElementById("provider-select");
44
- if (providerSelect && config.provider) {
45
- providerSelect.value = config.provider;
11
+ // Send on Enter
12
+ document.getElementById("user-input").addEventListener("keydown", (e) => {
13
+ if (e.key === "Enter" && !e.shiftKey) {
14
+ e.preventDefault();
15
+ sendMessage();
46
16
  }
17
+ });
47
18
 
48
- const updatePlaceholder = () => {
49
- const p = providerSelect.value;
50
- const input = document.getElementById("api-key-input");
51
- if (p === "openai") input.placeholder = "sk-proj-...";
52
- else if (p === "anthropic") input.placeholder = "sk-ant-api03-...";
53
- else input.placeholder = "sk-...";
54
- };
19
+ // Auto-resize the input box
20
+ document.getElementById("user-input").addEventListener("input", (e) => {
21
+ e.target.style.height = "auto";
22
+ e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px";
23
+ });
55
24
 
56
- providerSelect.addEventListener("change", updatePlaceholder);
57
- updatePlaceholder();
25
+ // Prompt click (Welcome message prompts)
26
+ const welcomeContent = document.getElementById("welcome-content");
27
+ if (welcomeContent) {
28
+ welcomeContent.addEventListener("click", (e) => {
29
+ const item = e.target.closest(".prompt-card");
30
+ if (!item) return;
31
+ const index = parseInt(item.dataset.index);
58
32
 
59
- // Show welcome view
60
- showWelcome(config);
61
- } catch (err) {
62
- console.error("Failed to load config:", err);
33
+ if (state.config && state.config.prompts[index]) {
34
+ const input = document.getElementById("user-input");
35
+ input.value = state.config.prompts[index];
36
+ input.focus();
37
+ input.style.height = "auto";
38
+ input.style.height = Math.min(input.scrollHeight, 120) + "px";
39
+ }
40
+ });
63
41
  }
64
42
  }
65
43
 
66
- function showWelcome(config) {
44
+ export function showWelcome(config) {
67
45
  const welcomeContent = document.getElementById("welcome-content");
68
46
 
69
47
  let promptsHtml = "";
@@ -94,94 +72,69 @@ function showWelcome(config) {
94
72
  }
95
73
  }
96
74
 
97
- function setupEventListeners() {
98
- // Send button
99
- document.getElementById("send-btn").addEventListener("click", sendMessage);
75
+ export async function sendMessage() {
76
+ const input = document.getElementById("user-input");
77
+ const text = input.value.trim();
78
+ if (!text) return;
100
79
 
101
- // Send on Enter
102
- document.getElementById("user-input").addEventListener("keydown", (e) => {
103
- if (e.key === "Enter" && !e.shiftKey) {
104
- e.preventDefault();
105
- sendMessage();
106
- }
107
- });
80
+ const chatArea = document.getElementById("chat-area");
81
+ if (chatArea.classList.contains("mode-welcome")) {
82
+ chatArea.classList.remove("mode-welcome");
83
+ chatArea.classList.add("mode-chat");
84
+ }
108
85
 
109
- // Auto-resize the input box
110
- document.getElementById("user-input").addEventListener("input", (e) => {
111
- e.target.style.height = "auto";
112
- e.target.style.height = Math.min(e.target.scrollHeight, 120) + "px";
113
- });
86
+ input.value = "";
87
+ input.style.height = "auto";
114
88
 
115
- // Save API key
116
- document.getElementById("save-key-btn").addEventListener("click", saveApiKey);
89
+ appendMessage("user", text);
90
+ chatHistory.push({ role: "user", content: text });
117
91
 
118
- // Prompt click
119
- const welcomeContent = document.getElementById("welcome-content");
120
- if (welcomeContent) {
121
- welcomeContent.addEventListener("click", (e) => {
122
- const item = e.target.closest(".prompt-card");
123
- if (!item) return;
124
- const index = parseInt(item.dataset.index);
92
+ const sendBtn = document.getElementById("send-btn");
93
+ sendBtn.disabled = true;
125
94
 
126
- // Get the full prompt text
127
- fetch(API_BASE + "/api/config")
128
- .then((r) => r.json())
129
- .then((config) => {
130
- if (config.prompts[index]) {
131
- const input = document.getElementById("user-input");
132
- input.value = config.prompts[index];
133
- input.focus();
134
- input.style.height = "auto";
135
- input.style.height = Math.min(input.scrollHeight, 120) + "px";
136
- }
137
- });
138
- });
95
+ currentAssistantMsg = appendMessage("assistant", "");
96
+ showLoadingIndicator();
97
+
98
+ try {
99
+ const socket = await getOrCreateWs();
100
+ socket.send(JSON.stringify({ text }));
101
+ } catch (err) {
102
+ handleError(err.message);
139
103
  }
140
104
  }
141
105
 
142
- async function saveApiKey() {
143
- const input = document.getElementById("api-key-input");
144
- const providerSelect = document.getElementById("provider-select");
145
- const status = document.getElementById("key-status");
146
- const key = input.value.trim();
147
- const provider = providerSelect ? providerSelect.value : "openai";
148
-
149
- if (!key) {
150
- status.textContent = "Enter an API key";
151
- status.className = "status-text error";
152
- return;
106
+ export async function sendBotCommand(cmdText) {
107
+ const chatArea = document.getElementById("chat-area");
108
+ if (chatArea.classList.contains("mode-welcome")) {
109
+ chatArea.classList.remove("mode-welcome");
110
+ chatArea.classList.add("mode-chat");
153
111
  }
154
112
 
155
- try {
156
- const res = await fetch(API_BASE + "/api/config/key", {
157
- method: "POST",
158
- headers: { "Content-Type": "application/json" },
159
- body: JSON.stringify({ key, provider }),
160
- });
113
+ appendMessage("user", cmdText);
114
+ chatHistory.push({ role: "user", content: cmdText });
161
115
 
162
- if (res.ok) {
163
- status.textContent = "API key saved";
164
- status.className = "status-text success";
165
- input.value = "";
166
- } else {
167
- status.textContent = "Save failed";
168
- status.className = "status-text error";
169
- }
116
+ const sendBtn = document.getElementById("send-btn");
117
+ sendBtn.disabled = true;
118
+
119
+ currentAssistantMsg = appendMessage("assistant", "");
120
+ showLoadingIndicator();
121
+
122
+ try {
123
+ const socket = await getOrCreateWs();
124
+ socket.send(JSON.stringify({ text: cmdText }));
170
125
  } catch (err) {
171
- status.textContent = "Save failed: " + err.message;
172
- status.className = "status-text error";
126
+ handleError(err.message);
173
127
  }
174
128
  }
175
129
 
176
- let ws = null;
177
- let currentAssistantMsg = null;
130
+ // ========== Internal Rendering & WS logic ==========
178
131
 
179
132
  function renderMarkdown(mdText, { renderEmbeddedMarkdown = true } = {}) {
180
- if (typeof marked === "undefined") {
133
+ if (typeof window.marked === "undefined") {
181
134
  return escapeHtml(mdText);
182
135
  }
183
136
 
184
- const html = ensureLinksOpenInNewTab(marked.parse(mdText));
137
+ const html = ensureLinksOpenInNewTab(window.marked.parse(mdText));
185
138
  if (!renderEmbeddedMarkdown) {
186
139
  return html;
187
140
  }
@@ -208,11 +161,9 @@ function renderEmbeddedMarkdownBlocks(html) {
208
161
  const codeBlocks = template.content.querySelectorAll("pre > code");
209
162
  codeBlocks.forEach((codeEl) => {
210
163
  const languageClass = Array.from(codeEl.classList).find((className) =>
211
- className.startsWith("language-"),
164
+ className.startsWith("language-")
212
165
  );
213
- const language = languageClass
214
- ? languageClass.slice("language-".length)
215
- : "";
166
+ const language = languageClass ? languageClass.slice("language-".length) : "";
216
167
 
217
168
  if (!/^(markdown|md)$/i.test(language)) {
218
169
  return;
@@ -240,11 +191,9 @@ async function getOrCreateWs() {
240
191
 
241
192
  return new Promise((resolve, reject) => {
242
193
  const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
243
- const providerSelect = document.getElementById("provider-select");
244
- const provider = providerSelect ? providerSelect.value : "openai";
194
+ const provider = state.config && state.config.provider ? state.config.provider : "openai";
245
195
 
246
- // URLSearchParams would be cleaner if more query params are added later
247
- const wsUrl = `${protocol}//${window.location.host}${API_BASE}/api/chat?provider=${provider}`;
196
+ const wsUrl = `${protocol}//${window.location.host}${state.API_BASE}/api/chat?provider=${provider}`;
248
197
 
249
198
  ws = new WebSocket(wsUrl);
250
199
 
@@ -327,12 +276,27 @@ function handleAgentEvent(event) {
327
276
 
328
277
  if (
329
278
  ["text_delta", "thinking_delta", "tool_start", "tool_end"].includes(
330
- event.type,
279
+ event.type
331
280
  )
332
281
  ) {
333
282
  hideLoadingIndicator();
334
283
  }
335
284
 
285
+ // Handle bot command results injected by the backend WebSocket response
286
+ if (event.type === "command_result") {
287
+ const textBlock = getOrCreateTextBlock();
288
+ let resText = `Command \`${event.command}\` succeeded.`;
289
+ if (event.errorMessage) {
290
+ resText = `Command \`${event.command}\` failed: ${event.errorMessage}`;
291
+ } else if (event.result) {
292
+ resText = `Command \`${event.command}\` result:\n\n${event.result}`;
293
+ }
294
+ textBlock.dataset.mdContent += resText;
295
+ textBlock.innerHTML = renderMarkdown(textBlock.dataset.mdContent);
296
+ scrollToBottom();
297
+ return;
298
+ }
299
+
336
300
  switch (event.type) {
337
301
  case "agent_start":
338
302
  case "message_start":
@@ -348,7 +312,7 @@ function handleAgentEvent(event) {
348
312
  const thinkingBlock = getOrCreateThinkingBlock();
349
313
  thinkingBlock.dataset.mdContent += event.delta;
350
314
  const contentEl = thinkingBlock.querySelector(".thinking-content");
351
- if (typeof marked !== "undefined") {
315
+ if (typeof window.marked !== "undefined") {
352
316
  contentEl.innerHTML = renderMarkdown(thinkingBlock.dataset.mdContent);
353
317
  } else {
354
318
  contentEl.textContent = thinkingBlock.dataset.mdContent;
@@ -359,7 +323,7 @@ function handleAgentEvent(event) {
359
323
  case "text_delta":
360
324
  const textBlock = getOrCreateTextBlock();
361
325
  textBlock.dataset.mdContent += event.delta;
362
- if (typeof marked !== "undefined") {
326
+ if (typeof window.marked !== "undefined") {
363
327
  textBlock.innerHTML = renderMarkdown(textBlock.dataset.mdContent);
364
328
  } else {
365
329
  textBlock.textContent = textBlock.dataset.mdContent;
@@ -376,9 +340,9 @@ function handleAgentEvent(event) {
376
340
  : JSON.stringify(event.toolInput, null, 2);
377
341
 
378
342
  let inputHtml = "";
379
- if (typeof marked !== "undefined") {
343
+ if (typeof window.marked !== "undefined") {
380
344
  inputHtml = ensureLinksOpenInNewTab(
381
- marked.parse("```json\n" + safeInput + "\n```"),
345
+ window.marked.parse("\`\`\`json\n" + safeInput + "\n\`\`\`")
382
346
  );
383
347
  } else {
384
348
  inputHtml = escapeHtml(safeInput);
@@ -387,7 +351,7 @@ function handleAgentEvent(event) {
387
351
  toolCard.innerHTML = `
388
352
  <div class="tool-header">
389
353
  <span class="tool-chevron">
390
- <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
354
+ <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
391
355
  </span>
392
356
  <span class="tool-icon">🛠️</span>
393
357
  <span class="tool-name">${escapeHtml(event.toolName)}</span>
@@ -403,9 +367,7 @@ function handleAgentEvent(event) {
403
367
  toolCard.classList.toggle("collapsed");
404
368
  });
405
369
 
406
- // Insert before loading indicator if exists
407
- const toolIndicator =
408
- currentAssistantMsg.querySelector(".loading-indicator");
370
+ const toolIndicator = currentAssistantMsg.querySelector(".loading-indicator");
409
371
  if (toolIndicator) {
410
372
  currentAssistantMsg.insertBefore(toolCard, toolIndicator);
411
373
  } else {
@@ -414,17 +376,12 @@ function handleAgentEvent(event) {
414
376
 
415
377
  toolCard.dataset.toolName = event.toolName;
416
378
  scrollToBottom();
417
-
418
379
  showLoadingIndicator();
419
380
  break;
420
381
 
421
382
  case "tool_end":
422
- const cards = Array.from(
423
- currentAssistantMsg.querySelectorAll(".tool-card.running"),
424
- );
425
- const card = cards
426
- .reverse()
427
- .find((c) => c.dataset.toolName === event.toolName);
383
+ const cards = Array.from(currentAssistantMsg.querySelectorAll(".tool-card.running"));
384
+ const card = cards.reverse().find((c) => c.dataset.toolName === event.toolName);
428
385
  if (card) {
429
386
  card.classList.remove("running");
430
387
  card.classList.add(event.isError ? "error" : "success");
@@ -448,17 +405,16 @@ function handleAgentEvent(event) {
448
405
  event.result &&
449
406
  typeof event.result === "string" &&
450
407
  (event.result.includes("\n") || event.result.length > 50)
451
- ? "```bash\n" + safeResult + "\n```"
452
- : "```json\n" + safeResult + "\n```";
408
+ ? "\`\`\`bash\n" + safeResult + "\n\`\`\`"
409
+ : "\`\`\`json\n" + safeResult + "\n\`\`\`";
453
410
 
454
- if (typeof marked !== "undefined") {
455
- resultEl.innerHTML = ensureLinksOpenInNewTab(marked.parse(mdText));
411
+ if (typeof window.marked !== "undefined") {
412
+ resultEl.innerHTML = ensureLinksOpenInNewTab(window.marked.parse(mdText));
456
413
  } else {
457
414
  resultEl.textContent = safeResult;
458
415
  }
459
416
  }
460
417
  scrollToBottom();
461
-
462
418
  showLoadingIndicator();
463
419
  break;
464
420
  }
@@ -466,7 +422,7 @@ function handleAgentEvent(event) {
466
422
 
467
423
  function getOrCreateThinkingBlock() {
468
424
  const children = Array.from(currentAssistantMsg.children).filter(
469
- (c) => !c.classList.contains("loading-indicator"),
425
+ (c) => !c.classList.contains("loading-indicator")
470
426
  );
471
427
  let lastChild = children[children.length - 1];
472
428
 
@@ -478,7 +434,7 @@ function getOrCreateThinkingBlock() {
478
434
  lastChild.innerHTML = `
479
435
  <div class="tool-header thinking-header">
480
436
  <span class="tool-chevron">
481
- <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
437
+ <svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
482
438
  </span>
483
439
  <span class="tool-icon">🧠</span>
484
440
  <span class="tool-name" style="color: var(--text-secondary);">Thinking Process</span>
@@ -502,7 +458,7 @@ function getOrCreateThinkingBlock() {
502
458
 
503
459
  function getOrCreateTextBlock() {
504
460
  const children = Array.from(currentAssistantMsg.children).filter(
505
- (c) => !c.classList.contains("loading-indicator"),
461
+ (c) => !c.classList.contains("loading-indicator")
506
462
  );
507
463
  let lastChild = children[children.length - 1];
508
464
 
@@ -527,40 +483,6 @@ function enableInput() {
527
483
  currentAssistantMsg = null;
528
484
  }
529
485
 
530
- async function sendMessage() {
531
- const input = document.getElementById("user-input");
532
- const text = input.value.trim();
533
- if (!text) return;
534
-
535
- const chatArea = document.getElementById("chat-area");
536
- if (chatArea.classList.contains("mode-welcome")) {
537
- chatArea.classList.remove("mode-welcome");
538
- chatArea.classList.add("mode-chat");
539
- }
540
-
541
- input.value = "";
542
- input.style.height = "auto";
543
-
544
- // Add the user message
545
- appendMessage("user", text);
546
- chatHistory.push({ role: "user", content: text });
547
-
548
- // Disable input while the agent is responding
549
- const sendBtn = document.getElementById("send-btn");
550
- sendBtn.disabled = true;
551
-
552
- // Create an assistant message placeholder
553
- currentAssistantMsg = appendMessage("assistant", "");
554
- showLoadingIndicator();
555
-
556
- try {
557
- const socket = await getOrCreateWs();
558
- socket.send(JSON.stringify({ text }));
559
- } catch (err) {
560
- handleError(err.message);
561
- }
562
- }
563
-
564
486
  function appendMessage(role, text) {
565
487
  const messages = document.getElementById("messages");
566
488
  const div = document.createElement("div");
@@ -591,6 +513,3 @@ function scrollToBottom() {
591
513
  const messages = document.getElementById("messages");
592
514
  messages.scrollTop = messages.scrollHeight;
593
515
  }
594
-
595
- // Start the app
596
- init();
@@ -0,0 +1,16 @@
1
+ export const state = {
2
+ config: null,
3
+ API_BASE: "",
4
+ restartRequired: false,
5
+ };
6
+
7
+ export async function loadConfig() {
8
+ try {
9
+ const res = await fetch(state.API_BASE + "/api/config");
10
+ state.config = await res.json();
11
+ return state.config;
12
+ } catch (err) {
13
+ console.error("Failed to load config:", err);
14
+ throw err;
15
+ }
16
+ }
@@ -0,0 +1,56 @@
1
+ import { state, loadConfig } from "./config.js";
2
+ import { initSettings } from "./settings.js";
3
+ import { initApiKeyDialog, updateApiKeyButton } from "./api-key-dialog.js";
4
+ import { initChatAppsDialog, updateChatAppsButton } from "./chat-apps-dialog.js";
5
+ import { initChat, showWelcome } from "./chat.js";
6
+
7
+ async function init() {
8
+ try {
9
+ const config = await loadConfig();
10
+
11
+ // Set Pack Name & Description
12
+ const elName = document.getElementById("pack-name");
13
+ const elDesc = document.getElementById("pack-desc");
14
+ if (elName) elName.textContent = config.name || "Skills Pack";
15
+ if (elDesc) elDesc.textContent = config.description || "";
16
+ document.title = config.name || "Skills Pack";
17
+
18
+ // Set Sidebar Skills list
19
+ const skillsList = document.getElementById("skills-list");
20
+ if (skillsList && config.skills) {
21
+ skillsList.innerHTML = config.skills
22
+ .map(
23
+ (s) =>
24
+ `<li><div class="skill-name">${s.name}</div><div class="skill-desc">${s.description}</div></li>`,
25
+ )
26
+ .join("");
27
+ }
28
+
29
+ // Pre-fill prompt if exactly one
30
+ if (config.prompts && config.prompts.length === 1) {
31
+ const input = document.getElementById("user-input");
32
+ if (input) {
33
+ input.value = config.prompts[0];
34
+ input.style.height = "auto";
35
+ input.style.height = Math.min(input.scrollHeight, 120) + "px";
36
+ }
37
+ }
38
+
39
+ showWelcome(config);
40
+ } catch (err) {
41
+ console.error("Initialization Failed:", err);
42
+ }
43
+
44
+ // Initialize all dialog modules
45
+ initSettings();
46
+ initApiKeyDialog();
47
+ initChatAppsDialog();
48
+ initChat();
49
+
50
+ // Update action button states based on config
51
+ updateApiKeyButton();
52
+ updateChatAppsButton();
53
+ }
54
+
55
+ // Start application
56
+ init();