samagotchi 0.131.0 → 0.133.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +88 -1
  3. data/bin/chi +19 -1
  4. data/docs/architecture.md +59 -1
  5. data/docs/cli.md +29 -8
  6. data/docs/configuration.md +115 -16
  7. data/docs/desktop.md +16 -6
  8. data/docs/guardrails.md +10 -4
  9. data/docs/internals/anthropic.md +3 -2
  10. data/docs/internals/engine.md +4 -4
  11. data/docs/internals/prompt-caching.md +26 -6
  12. data/docs/memory.md +16 -2
  13. data/docs/plugins.md +68 -1
  14. data/docs/sessions.md +60 -6
  15. data/docs/testing.md +1 -1
  16. data/lib/samagotchi/bootstrap_command.rb +35 -6
  17. data/lib/samagotchi/bundles/mcp/manifest.yml +2 -2
  18. data/lib/samagotchi/bundles/mcp/plugin.rb +10 -3
  19. data/lib/samagotchi/bundles/model-notes/manifest.yml +4 -1
  20. data/lib/samagotchi/bundles/model-notes/model_notes_tone.md +3 -0
  21. data/lib/samagotchi/bundles/model-notes/notes/README.md +14 -0
  22. data/lib/samagotchi/bundles/system/manifest.yml +1 -1
  23. data/lib/samagotchi/cache_confirm.rb +18 -0
  24. data/lib/samagotchi/chi_home.rb +311 -0
  25. data/lib/samagotchi/chi_shim.rb +77 -0
  26. data/lib/samagotchi/child_env.rb +5 -3
  27. data/lib/samagotchi/config.rb +2 -0
  28. data/lib/samagotchi/context_fetch.rb +4 -2
  29. data/lib/samagotchi/desktop/macos.rb +100 -18
  30. data/lib/samagotchi/engine.rb +35 -4
  31. data/lib/samagotchi/engine_snapshots.rb +5 -0
  32. data/lib/samagotchi/gem_update.rb +19 -10
  33. data/lib/samagotchi/guardrail_wiring.rb +3 -1
  34. data/lib/samagotchi/guardrails/pair_boundary.rb +21 -2
  35. data/lib/samagotchi/guardrails/pair_command.rb +57 -1
  36. data/lib/samagotchi/guardrails/protected_paths.rb +6 -3
  37. data/lib/samagotchi/guardrails/review_paths.rb +7 -4
  38. data/lib/samagotchi/guardrails/verdict.rb +1 -1
  39. data/lib/samagotchi/home_command.rb +145 -0
  40. data/lib/samagotchi/home_doctor.rb +177 -0
  41. data/lib/samagotchi/home_notice.rb +80 -0
  42. data/lib/samagotchi/installed_versions.rb +5 -3
  43. data/lib/samagotchi/llm_context_command.rb +53 -7
  44. data/lib/samagotchi/llm_context_override.rb +3 -2
  45. data/lib/samagotchi/plugin/api.rb +45 -1
  46. data/lib/samagotchi/plugin/context.rb +34 -1
  47. data/lib/samagotchi/plugin/loader.rb +5 -2
  48. data/lib/samagotchi/plugin/prompt_sections.rb +124 -0
  49. data/lib/samagotchi/project_env.rb +360 -0
  50. data/lib/samagotchi/run_options.rb +1 -0
  51. data/lib/samagotchi/self_report.rb +33 -0
  52. data/lib/samagotchi/session_access/contract.rb +1 -1
  53. data/lib/samagotchi/session_commands.rb +68 -19
  54. data/lib/samagotchi/session_manager.rb +54 -1
  55. data/lib/samagotchi/session_model_settings.rb +5 -2
  56. data/lib/samagotchi/system_prompt.rb +34 -3
  57. data/lib/samagotchi/terminal_ui/attach_launcher.rb +3 -2
  58. data/lib/samagotchi/terminal_ui/formatting.rb +3 -0
  59. data/lib/samagotchi/testing/boundary.rb +3 -1
  60. data/lib/samagotchi/testing/bundle.rb +22 -3
  61. data/lib/samagotchi/testing/host.rb +22 -1
  62. data/lib/samagotchi/testing/recorder.rb +6 -1
  63. data/lib/samagotchi/testing/recording_api.rb +1 -1
  64. data/lib/samagotchi/tools/builtins.rb +13 -2
  65. data/lib/samagotchi/update_command.rb +65 -7
  66. data/lib/samagotchi/version.rb +1 -1
  67. data/lib/samagotchi/web/app.rb +61 -1
  68. data/lib/samagotchi/web/public/app.js +4 -1
  69. data/lib/samagotchi/web/public/chip_popover.js +5 -2
  70. data/lib/samagotchi/web/public/composer.css +11 -1
  71. data/lib/samagotchi/web/public/data.js +8 -4
  72. data/lib/samagotchi/web/public/index.html +52 -10
  73. data/lib/samagotchi/web/public/level_chip.js +22 -38
  74. data/lib/samagotchi/web/public/llm_context_chip.js +30 -73
  75. data/lib/samagotchi/web/public/model_pick.js +26 -0
  76. data/lib/samagotchi/web/public/model_picker.js +3 -1
  77. data/lib/samagotchi/web/public/review_start.js +13 -57
  78. data/lib/samagotchi/web/public/send_turn.js +3 -2
  79. data/lib/samagotchi/web/public/start_page.js +131 -99
  80. data/lib/samagotchi/web/public/start_settings.js +298 -0
  81. data/lib/samagotchi/web/public/start_toolkit.js +157 -0
  82. data/lib/samagotchi/web/public/thinking_chip.js +9 -22
  83. data/lib/samagotchi/web/server.rb +27 -8
  84. data/lib/samagotchi/web/update_check.rb +3 -2
  85. metadata +11 -1
@@ -7,10 +7,9 @@
7
7
  // worker's snapshot, a command_ran, or the server's own reading of the
8
8
  // session file).
9
9
  //
10
- // The start page has one too (kind "new session"): it shows the picked
11
- // model's values (GET /api/models' llm_context) and its form keeps a
12
- // choice for the next create only (POST /api/sessions' llm_context), never
13
- // remembered: nothing is sent unless the user sets one.
10
+ // The start page's new session sets its strategy in the ⚙ panel instead
11
+ // (start_settings.js): mergeChoice and choiceWords are its, a choice in the
12
+ // session file's shape sent as POST /api/sessions' llm_context words.
14
13
 
15
14
  import { createChipPopover } from "./chip_popover.js";
16
15
  import { html, nothing, render } from "./vendor/lit-html.js";
@@ -22,7 +21,6 @@ const STRATEGIES = [
22
21
  ];
23
22
  export const APPLIES = ["payoff", "next_request", "turn_end"];
24
23
  export const SESSION = "session";
25
- export const NEW_SESSION = "new session";
26
24
 
27
25
  const DEFAULT = "default";
28
26
  const LAYER_ORDER = ["stale", "forget"];
@@ -43,34 +41,15 @@ export function ownValues(summary) {
43
41
  return { strategy, apply: own.apply || DEFAULT, budget };
44
42
  }
45
43
 
46
- // The start page's choice for the new session ({strategy: [layers], apply,
47
- // budget_tokens}, the session file's shape; 0 is off) laid over the
48
- // model's summary: what the chip shows, with the chosen fields' source
49
- // "session" ("the new session"). The model's own summary without a choice.
50
- export function withChoice(summary, choice) {
51
- if (!summary || !choice) return summary;
52
- const merged = { ...summary, own: choice };
53
- // (the budget's source and where are budget_source / budget_where)
54
- const set = (field, key, value) => Object.assign(merged, { [key]: value, [`${field}_source`]: SESSION,
55
- [`${field}_where`]: "the new session" });
56
- if (Array.isArray(choice.strategy)) set("strategy", "strategy", strategyValue(choice.strategy));
57
- if (choice.apply) set("apply", "apply", choice.apply);
58
- if (typeof choice.budget_tokens === "number") {
59
- set("budget", "budget_tokens", choice.budget_tokens > 0 ? choice.budget_tokens : null);
60
- }
61
- return merged;
62
- }
63
-
64
- // The chip: its text, tooltip and whether the session (or the new session)
65
- // set its own; null without a summary (an older worker).
66
- export function chipModel(summary, { kind = SESSION } = {}) {
44
+ // The chip: its text, tooltip and whether the session set its own; null
45
+ // without a summary (an older worker).
46
+ export function chipModel(summary) {
67
47
  if (!summary || !summary.strategy) return null;
68
48
  const own = summary.strategy_source === "session" || !!summary.own;
69
49
  const budget = summary.budget_tokens ? `${summary.budget_tokens} tokens` : "off";
70
- const what = kind === NEW_SESSION ? "the new session" : "this session";
71
50
  const title = `LLM context strategy: ${summary.strategy} (${summary.strategy_where}); apply ${summary.apply} ` +
72
- `(${summary.apply_where}); budget ${budget} (${summary.budget_where}). Click to change it for ${what}.`;
73
- return { text: `llm ctx ${summary.strategy}${own ? ` · ${kind}` : ""}`, title, own };
51
+ `(${summary.apply_where}); budget ${budget} (${summary.budget_where}). Click to change it for this session.`;
52
+ return { text: `llm ctx ${summary.strategy}${own ? ` · ${SESSION}` : ""}`, title, own };
74
53
  }
75
54
 
76
55
  // The session bar's chip (a lit-html template; info_bar.js), nothing
@@ -85,14 +64,15 @@ export function chipTemplate(summary) {
85
64
  // the form opened with) only the fields the user changed go, so a value the
86
65
  // form can't show is never unset by a Set; null when nothing changed.
87
66
  // Without it every field goes, "default" for one left to the model, and all
88
- // of them default is a reset.
67
+ // of them default is a reset. With --yes: the form states what a change
68
+ // costs, so a Set is the choice (no confirm step), as level_chip.js's.
89
69
  export function commandLine({ strategy = DEFAULT, apply = DEFAULT, budget = "" } = {}, initial = null) {
90
70
  const words = { strategy, apply, budget: String(budget).trim() || DEFAULT };
91
71
  const was = initial && { strategy: initial.strategy, apply: initial.apply, budget: String(initial.budget).trim() || DEFAULT };
92
72
  const fields = Object.keys(words).filter((field) => !was || words[field] !== was[field]);
93
73
  if (!fields.length) return null;
94
- if (!was && fields.every((field) => words[field] === DEFAULT)) return "/llm-context reset";
95
- return `/llm-context ${fields.map((field) => `${field} ${words[field]}`).join(" ")}`;
74
+ if (!was && fields.every((field) => words[field] === DEFAULT)) return "/llm-context reset --yes";
75
+ return `/llm-context ${fields.map((field) => `${field} ${words[field]}`).join(" ")} --yes`;
96
76
  }
97
77
 
98
78
  // A budget as typed ("64000", "64k", "off", "0") in tokens, 0 for off;
@@ -107,10 +87,10 @@ export function parseBudget(text) {
107
87
  return tokens >= 4000 && tokens <= 10_000_000 ? tokens : null;
108
88
  }
109
89
 
110
- // The new session's choice after a Set: +choice+ with the fields the user
111
- // changed (vs +initial+, the values the form opened with) applied, a field
112
- // set back to default dropped; null when nothing is left. Throws on a
113
- // budget that isn't one.
90
+ // The new session's choice (start_settings.js) after a pick or a Set:
91
+ // +choice+ with the fields the user changed (vs +initial+, the values
92
+ // before) applied, a field set back to default dropped; null when nothing
93
+ // is left. Throws on a budget that isn't one.
114
94
  export function mergeChoice(choice, { strategy = DEFAULT, apply = DEFAULT, budget = "" } = {}, initial = null) {
115
95
  const next = { ...(choice || {}) };
116
96
  const words = { strategy, apply, budget: String(budget).trim() || DEFAULT };
@@ -160,23 +140,18 @@ export function nowText(summary) {
160
140
  `budget ${budget} (${summary.budget_where})`;
161
141
  }
162
142
 
163
- // The popover's form (a lit-html template). For the new session (kind
164
- // NEW_SESSION), +summary+ is the model's and +choice+ the values the form
165
- // shows; +now+ is the summary its "Now:" line reads (refresh passes a newer
166
- // one, the rest stays as the form opened, so lit leaves what the user typed
167
- // alone); +error+ a failed Set's message. The selects take their value as
168
- // a property (.value), set after their options: +unset+ draws the options
169
- // alone, the first pass renderForm makes.
170
- export function formTemplate(summary, { running = false, kind = SESSION, choice = null, now = summary, error = null,
171
- unset = false } = {}) {
172
- const fresh = kind === NEW_SESSION;
173
- const own = fresh ? ownValues({ own: choice }) : ownValues(summary);
143
+ // The popover's form (a lit-html template). +now+ is the summary its "Now:"
144
+ // line reads (refresh passes a newer one, the rest stays as the form opened,
145
+ // so lit leaves what the user typed alone); +error+ a failed Set's message.
146
+ // The selects take their value as a property (.value), set after their
147
+ // options: +unset+ draws the options alone, the first pass renderForm makes.
148
+ export function formTemplate(summary, { running = false, now = summary, error = null, unset = false } = {}) {
149
+ const own = ownValues(summary);
174
150
  const follow = (field) => (summary[`${field}_source`] === "session" ? "follow the model" : `follow the model (${summary[field]})`);
175
- const when = fresh ? "For the new session's first turn on; not remembered for the next one."
176
- : "From the next turn's start. Turning forget on or off re-reads the whole prompt once.";
151
+ const when = "From the next turn's start. Turning forget on or off re-reads the whole prompt once.";
177
152
  const chosen = (value) => (unset ? nothing : value);
178
153
  return html`<form class="llmctx-form"><div class="ctx-pop-head"><strong>LLM context</strong
179
- ><span class="ctx-pop-kind">${fresh ? NEW_SESSION : "this session"}</span
154
+ ><span class="ctx-pop-kind">this session</span
180
155
  ><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div
181
156
  ><div class="ctx-pop-meta llmctx-now">${nowText(now)}</div
182
157
  ><label class="llmctx-field"><span>Strategy</span><span class="select-wrap"><select name="strategy" .value=${chosen(own.strategy)}>${
@@ -205,16 +180,11 @@ export function renderForm(target, summary, opts = {}) {
205
180
  // The chip's popover (chip_popover.js' shell). el: where the chip is drawn
206
181
  // (#infoText, info_bar.js: clicks are delegated); summary(): the open
207
182
  // session's llm_context; session(): the open session's id; running(): a
208
- // turn runs; run(line): sends the command.
209
- // The start page's (kind NEW_SESSION): summary() is the picked model's,
210
- // choice() the pending choice, and choose(next) takes a Set's (null for
211
- // "follow the model") instead of a command running. id and anchor (the
212
- // chip's selector) keep the two controls apart.
183
+ // turn runs; run(line): sends the command. id and anchor: the popover's id
184
+ // and the chip's selector.
213
185
  export function createLlmContextControl({ el, summary, session = () => null, running = () => false, run,
214
- kind = SESSION, choice = () => null, choose = null,
215
186
  id = "llmContextPopover", anchor = ".llmctx-chip",
216
187
  doc = document, win = window }) {
217
- const fresh = kind === NEW_SESSION;
218
188
  // The values the form opened with.
219
189
  let initial = null;
220
190
  // What the open form was drawn from: the summary, the form's options and
@@ -230,26 +200,14 @@ export function createLlmContextControl({ el, summary, session = () => null, run
230
200
  // line alone).
231
201
  function show(data, opening) {
232
202
  if (!opening) return draw({ now: data });
233
- initial = fresh ? ownValues({ own: choice() }) : ownValues(data);
234
- shown = { data, opts: { running: running(), kind, choice: choice() } };
203
+ initial = ownValues(data);
204
+ shown = { data, opts: { running: running() } };
235
205
  draw({ now: data, error: null });
236
206
  }
237
207
 
238
208
  function submit(form) {
239
209
  const values = { strategy: form.elements.strategy.value, apply: form.elements.apply.value,
240
210
  budget: form.elements.budget.value };
241
- if (fresh) {
242
- let next;
243
- try {
244
- next = mergeChoice(choice(), values, initial);
245
- } catch (err) {
246
- draw({ error: err.message });
247
- return;
248
- }
249
- shell.close();
250
- choose(next);
251
- return;
252
- }
253
211
  const line = commandLine(values, initial);
254
212
  shell.close();
255
213
  if (line) run(line);
@@ -258,8 +216,7 @@ export function createLlmContextControl({ el, summary, session = () => null, run
258
216
  function act(name) {
259
217
  if (name !== "reset") return;
260
218
  shell.close();
261
- if (fresh) choose(null);
262
- else run(commandLine());
219
+ run(commandLine());
263
220
  }
264
221
 
265
222
  const shell = createChipPopover({
@@ -58,6 +58,32 @@ export function modelRows(payload) {
58
58
  });
59
59
  }
60
60
 
61
+ // A host-less server fallback may still carry "host:org/id".
62
+ function shortNameCandidate(row) {
63
+ const name = String(row?.name || row?.id || "");
64
+ let host = row?.host ? String(row.host) : "";
65
+ let id = String(row?.id || name);
66
+ if (!host) {
67
+ const colon = name.indexOf(":");
68
+ const slash = name.indexOf("/");
69
+ if (colon > 0 && slash > colon) {
70
+ host = name.slice(0, colon);
71
+ id = name.slice(colon + 1);
72
+ }
73
+ }
74
+ const model = id.slice(id.lastIndexOf("/") + 1) || name;
75
+ const labelHost = host && (!row?.isDefaultHost || !row?.host) ? `${host} · ` : "";
76
+ return { name, label: `${labelHost}${model}` };
77
+ }
78
+
79
+ // A compact label for the chosen model; list rows and all stored/API names
80
+ // remain full.
81
+ export function shortName(row, rows = []) {
82
+ const candidate = shortNameCandidate(row);
83
+ const clashes = rows.filter((other) => shortNameCandidate(other).label === candidate.label).length;
84
+ return clashes > 1 ? candidate.name : candidate.label;
85
+ }
86
+
61
87
  const byId = (a, b) => (b.configured === true) - (a.configured === true) ||
62
88
  a.id.localeCompare(b.id, "en", { sensitivity: "base" });
63
89
 
@@ -13,6 +13,7 @@ import { moveSelection } from "./command_complete.js";
13
13
  import { readStored, writeStored } from "./storage.js";
14
14
  import {
15
15
  MODEL_KEY, RECENT_KEY, groupRows, matchModels, modelRows, pickModel, pushRecent, recentRows, rowNote, rowTitle,
16
+ shortName,
16
17
  } from "./model_pick.js";
17
18
  import { html, nothing, render as draw } from "./vendor/lit-html.js";
18
19
 
@@ -63,7 +64,8 @@ export function createModelPicker({ button, panel, input, list, composer, center
63
64
  // app.js', carries the host's warning).
64
65
  function setChosen(name) {
65
66
  chosen = name;
66
- button.querySelector(".model-pick-name").textContent = name;
67
+ const row = rows.find((item) => item.name === name);
68
+ button.querySelector(".model-pick-name").textContent = row ? shortName(row, rows) : name;
67
69
  button.classList.toggle("unavailable", rows.some((r) => r.name === name && r.unavailable === true));
68
70
  }
69
71
 
@@ -1,10 +1,10 @@
1
1
  // Start review (the web's entry points to a review session, docs/sessions.md
2
- // Review sessions): the start page's review chip, whose files make the new
3
- // session a review session (POST /api/sessions {review: {subjects}}), and the
4
- // session bar's review button, a review fork of the open session
5
- // ({review: {from}}): its recap, not its transcript; it keeps running.
6
- import { html, nothing, render } from "./vendor/lit-html.js";
7
- import { createChipPopover } from "./chip_popover.js";
2
+ // Review sessions): the start page's review files (the ⚙ panel's review
3
+ // view, start_settings.js), which make the new session a review session
4
+ // (POST /api/sessions {review: {subjects}}), and the session bar's review
5
+ // button, a review fork of the open session ({review: {from}}): its recap,
6
+ // not its transcript; it keeps running.
7
+ import { html, nothing } from "./vendor/lit-html.js";
8
8
 
9
9
  // The files typed: split on commas, spaces and new lines, no repeats; a
10
10
  // path with a space (or a comma) in double quotes ("docs/my plan.md").
@@ -25,7 +25,7 @@ export function formatSubjects(subjects) {
25
25
  return (subjects || []).map((s) => (/[\s,"]/.test(s) ? `"${s}"` : s)).join(", ");
26
26
  }
27
27
 
28
- // The start page's chip: "review…" with none, else the files' names.
28
+ // The start page's override chip: "review…" with none, else the files' names.
29
29
  export function reviewChipModel(subjects) {
30
30
  const list = subjects || [];
31
31
  if (!list.length) {
@@ -37,9 +37,12 @@ export function reviewChipModel(subjects) {
37
37
  }
38
38
 
39
39
  // +error+: why the files weren't taken (a quote left open, the server's
40
- // refusal at Start), shown above Set; +text+ the form's text as typed.
41
- export function reviewFormTemplate(subjects, { error = null, text = null } = {}) {
42
- return html`<div class="ctx-pop-head"><strong>Review</strong><span class="ctx-pop-kind">new session</span
40
+ // refusal at Start), shown above Set; +text+ the form's text as typed;
41
+ // +back+: a ‹ before the heading (data-act="back": the start page's ⚙
42
+ // panel, start_settings.js, whose view this is).
43
+ export function reviewFormTemplate(subjects, { error = null, text = null, back = false } = {}) {
44
+ return html`<div class="ctx-pop-head">${back ? html`<button type="button" class="ss-back" data-act="back"
45
+ aria-label="Back to the settings" title="Back to the settings">‹</button>` : nothing}<strong>Review</strong><span class="ctx-pop-kind">new session</span
43
46
  ><button type="button" class="ctx-pop-close" data-act="close" aria-label="Close">×</button></div>
44
47
  <div class="ctx-pop-meta" id="reviewNewHelp">The files participants read beside the thread: project paths, relative to this folder,
45
48
  a path with a space in quotes. Secrets (.env, keys) and files outside the project are refused.</div>
@@ -50,50 +53,3 @@ export function reviewFormTemplate(subjects, { error = null, text = null } = {})
50
53
  <div class="ctx-pop-actions">${subjects?.length ? html`<button type="button" data-act="clear">No review</button>` : nothing}<button
51
54
  type="submit">Set</button></div></form>`;
52
55
  }
53
-
54
- // The start page's chip and its form. +el+ the chip; +choose(subjects)+.
55
- // refuse(message): Start's refusal of the files (400 invalid_review), said
56
- // in the form, opened again at the files chosen.
57
- export function createReviewStartControl({ el, choice, choose, doc = document, win = window }) {
58
- let error = null;
59
- let text = null;
60
- const draw = () => {
61
- render(reviewFormTemplate(choice(), { error, text }), shell.pop);
62
- shell.replace();
63
- };
64
- const shell = createChipPopover({
65
- id: "reviewNewPopover", className: "ctx-popover review-popover", width: 360, el, anchor: `#${el.id}`,
66
- summary: () => ({}), sheet: true, label: "Review files", doc, win,
67
- show: () => {
68
- draw();
69
- shell.pop.querySelector("textarea")?.focus();
70
- },
71
- onClose: () => { error = null; text = null; },
72
- onSubmit: (form) => {
73
- const typed = form.elements.subjects?.value || "";
74
- const read = readSubjects(typed);
75
- if (read.error) {
76
- error = read.error;
77
- text = typed;
78
- return draw();
79
- }
80
- choose(read.subjects);
81
- shell.close();
82
- },
83
- onAct: (name) => {
84
- if (name === "clear") {
85
- choose([]);
86
- shell.close();
87
- }
88
- },
89
- });
90
- return {
91
- close: () => shell.close(),
92
- refuse: (message) => {
93
- error = message;
94
- text = null;
95
- if (shell.isOpen()) draw();
96
- else shell.open(el);
97
- },
98
- };
99
- }
@@ -54,10 +54,11 @@ export function createSendTurn({
54
54
  }
55
55
  }
56
56
 
57
- // A create's refusal: the review files' in the review form (the start
58
- // page's chip), else an alert.
57
+ // A create's refusal: the review files' in the ⚙ panel's review view, a
58
+ // picked kit's in the 🧰 list, else an alert.
59
59
  function refused(e) {
60
60
  if (e?.code === "invalid_review" && startPage.reviewRefused?.(e.message)) return;
61
+ if (e?.code === "toolkit_refused" && startPage.toolkitRefused?.(e.message)) return;
61
62
  alert(e.message);
62
63
  }
63
64