memoryai-claude 0.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.
- package/LICENSE +21 -0
- package/README.md +69 -0
- package/dist/cli.js +28 -0
- package/dist/session-start-runner.js +3 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MemoryAI
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# MemoryAI for Claude Code
|
|
2
|
+
|
|
3
|
+
Your AI keeps forgetting you. MemoryAI gives Claude Code a real long-term
|
|
4
|
+
memory: one that follows you across every model.
|
|
5
|
+
|
|
6
|
+
```text
|
|
7
|
+
You move models. You move IDEs. Your memory stays.
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Install (one command)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npx memoryai-claude install
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
That's it. After it finishes, restart Claude Code once and just work. Past
|
|
17
|
+
decisions, preferences, and recent project context come back at the start of
|
|
18
|
+
each prompt; important moments save automatically when each turn ends.
|
|
19
|
+
|
|
20
|
+
## Get a key
|
|
21
|
+
|
|
22
|
+
Pick whichever is easier:
|
|
23
|
+
|
|
24
|
+
- Skip the prompt and a free key is created for you on install
|
|
25
|
+
- Or grab one upfront at https://memoryai.dev/connect and paste it
|
|
26
|
+
|
|
27
|
+
## Health check
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npx memoryai-claude doctor
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Confirms the three hooks are wired and that each endpoint actually responds.
|
|
34
|
+
|
|
35
|
+
## Status
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npx memoryai-claude status
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Tells you how many hooks are wired in user-scope and project-scope settings.
|
|
42
|
+
|
|
43
|
+
## Uninstall
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npx memoryai-claude uninstall
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Removes the hooks from `settings.json`. Foreign hooks stay untouched. Memory
|
|
50
|
+
on the server is preserved unless you delete it from the dashboard.
|
|
51
|
+
|
|
52
|
+
## What gets touched
|
|
53
|
+
|
|
54
|
+
- `~/.claude/settings.json` (or `./.claude/settings.json` with `--project`)
|
|
55
|
+
- `~/.claude/CLAUDE.md` (or `./CLAUDE.md` with `--project`) — a short note
|
|
56
|
+
is appended so the agent knows memory is wired.
|
|
57
|
+
|
|
58
|
+
The original file is backed up before each write.
|
|
59
|
+
|
|
60
|
+
## Privacy
|
|
61
|
+
|
|
62
|
+
- API keys live in your local `settings.json`. They never leave your machine
|
|
63
|
+
except to talk to the endpoint you configured.
|
|
64
|
+
- No telemetry from this CLI itself.
|
|
65
|
+
- Add `MEMORYAI_NONINTERACTIVE=1` to skip prompts during scripted installs.
|
|
66
|
+
|
|
67
|
+
## License
|
|
68
|
+
|
|
69
|
+
MIT.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var a=require("node:fs"),m=require("node:path"),X=require("node:os");var F=require("node:readline/promises"),S=require("node:process");async function $(e,o){let t=(0,F.createInterface)({input:S.stdin,output:S.stdout});try{let n=o?` [${o}]`:"";return(await t.question(`${e}${n}: `)).trim()||o||""}finally{t.close()}}async function R(e,o=!0){return(await $(`${e} (${o?"Y/n":"y/N"})`,o?"y":"n")).toLowerCase().startsWith("y")}function k(){return process.env.MEMORYAI_NONINTERACTIVE==="1"||process.env.CI==="true"||!process.stdin.isTTY}var l=require("node:fs"),g=require("node:path"),x=require("node:os");function v(e){return e==="project"?(0,g.join)(process.cwd(),".claude","settings.json"):(0,g.join)((0,x.homedir)(),".claude","settings.json")}function N(e){return e==="project"?(0,g.join)(process.cwd(),"CLAUDE.md"):(0,g.join)((0,x.homedir)(),".claude","CLAUDE.md")}function w(e){if(!(0,l.existsSync)(e))return{};try{return JSON.parse((0,l.readFileSync)(e,"utf-8"))||{}}catch{throw new Error(`${e} is not valid JSON. Fix it manually before re-running.`)}}function I(e,o){(0,l.mkdirSync)((0,g.dirname)(e),{recursive:!0}),(0,l.writeFileSync)(e,JSON.stringify(o,null,2)+`
|
|
3
|
+
`,"utf-8")}function M(e){if(!(0,l.existsSync)(e))return null;let o=new Date().toISOString().replace(/[:.]/g,"-"),t=`${e}.bak-${o}`;return(0,l.copyFileSync)(e,t),t}var oe="/v1/hooks/claude/",te="memoryai/session-start-runner";function H(e,o,t){return{type:"http",url:e,timeout:t,headers:{Authorization:`Bearer ${o}`},allowedEnvVars:[]}}function K(e,o,t){return{type:"command",command:e,args:[o],timeout:t}}function D(e){if(!e)return!1;if(typeof e.url=="string"&&e.url.includes(oe))return!0;if(typeof e.command=="string"){let o=Array.isArray(e.args)?e.args.join(" "):"",t=`${e.command} ${o}`;if(t.includes(te)||t.includes("session-start-runner"))return!0}return!1}function J(e){return(e&&e.hooks||[]).some(D)}function A(e,o,t){return e.hooks=e.hooks||{},e.hooks[o]=e.hooks[o]||[],e.hooks[o].some(J)?!1:(e.hooks[o].push({hooks:[t]}),!0)}function L(e){if(!e.hooks)return 0;let o=0;for(let t of Object.keys(e.hooks)){let n=(e.hooks[t]||[]).length;e.hooks[t]=(e.hooks[t]||[]).filter(r=>!J(r)),o+=n-e.hooks[t].length,e.hooks[t].length===0&&delete e.hooks[t]}return Object.keys(e.hooks).length===0&&delete e.hooks,o}function O(e){let o={SessionStart:{present:!1},UserPromptSubmit:{present:!1},Stop:{present:!1}};for(let t of Object.keys(o)){let n=e?.hooks?.[t]||[];for(let r of n){let s=(r.hooks||[]).find(D);if(s){o[t]={present:!0,url:s.url,command:s.command?`${s.command} ${(s.args||[]).join(" ")}`.trim():void 0,timeout:s.timeout};break}}}return o}var p=require("node:fs"),P="<!-- memoryai:auto-note -->",Y=`
|
|
4
|
+
${P}
|
|
5
|
+
## MemoryAI
|
|
6
|
+
|
|
7
|
+
Memory works automatically here. Past decisions, preferences, and recent project
|
|
8
|
+
context are recalled before each prompt and saved when each turn ends. Nothing
|
|
9
|
+
to call by hand \u2014 just work normally.
|
|
10
|
+
`;function z(e){let o=(0,p.existsSync)(e)?(0,p.readFileSync)(e,"utf-8"):"";if(o.includes(P))return"skipped";let t=o?`${o.replace(/\s*$/,"")}
|
|
11
|
+
${Y}`:Y;return(0,p.writeFileSync)(e,t,"utf-8"),o?"appended":"created"}function B(e){if(!(0,p.existsSync)(e))return!1;let o=(0,p.readFileSync)(e,"utf-8");if(!o.includes(P))return!1;let t=o.replace(new RegExp(`\\n*${P}[\\s\\S]*$`,"m"),"").replace(/\s*$/,`
|
|
12
|
+
`);return(0,p.writeFileSync)(e,t,"utf-8"),!0}async function G(e,o){let t=e.replace(/\/+$/,"");try{return(await fetch(`${t}/v1/stats`,{method:"GET",headers:{Authorization:`Bearer ${o}`}})).ok}catch{return!1}}async function V(e,o){let t=e.replace(/\/+$/,"");try{let n=await fetch(`${t}/v1/admin/provision`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({name:o||"claude-code",tos_accepted:!0})});if(!n.ok)return null;let r=await n.json();return r&&typeof r.api_key=="string"?{api_key:r.api_key,plan:r.plan}:null}catch{return null}}async function q(e,o){let t=e.replace(/\/+$/,""),n={},r=[{event:"SessionStart",path:"/v1/ide/guard/bootstrap",body:{task:"",limit:1}},{event:"UserPromptSubmit",path:"/v1/hooks/claude/user-prompt",body:{prompt:"health check from memoryai-claude doctor"}},{event:"Stop",path:"/v1/hooks/claude/stop",body:{last_assistant_message:""}}];for(let{event:s,path:c,body:f}of r)try{let u=await fetch(`${t}${c}`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${o}`},body:JSON.stringify(f)});n[s]=u.ok}catch{n[s]=!1}return n}var Z="0.1.1",W="https://memoryai.dev",j=(0,m.join)((0,X.homedir)(),".memoryai"),E=(0,m.join)(j,"claude.json"),_=(0,m.join)(j,"session-start-runner.js");function ne(e){let o={command:e[0]||"help"};for(let t=1;t<e.length;t++){let n=e[t];n==="--user"?o.scope="user":n==="--project"?o.scope="project":n==="--endpoint"?o.endpoint=e[++t]:n==="--key"?o.key=e[++t]:(n==="--yes"||n==="-y")&&(o.yes=!0)}return o}function h(){console.log(`MemoryAI for Claude Code v${Z}`),console.log("One brain. Every AI you use. Forever."),console.log("")}function Q(){h(),console.log(`Usage:
|
|
13
|
+
memoryai-claude install [--user|--project] [--endpoint URL] [--key KEY] [--yes]
|
|
14
|
+
memoryai-claude doctor
|
|
15
|
+
memoryai-claude status
|
|
16
|
+
memoryai-claude logs
|
|
17
|
+
memoryai-claude uninstall [--user|--project]
|
|
18
|
+
memoryai-claude help
|
|
19
|
+
|
|
20
|
+
Memory works automatically once installed: relevant context comes back at the
|
|
21
|
+
start of each prompt, and important moments save when each turn ends. There is
|
|
22
|
+
nothing to call by hand.
|
|
23
|
+
|
|
24
|
+
Get a free key at https://memoryai.dev (or leave it blank during install and a
|
|
25
|
+
key will be created for you).
|
|
26
|
+
`)}async function ee(e){return e.scope?e.scope:k()?"user":(await $("Apply to (u)ser globally or this (p)roject?","u")).toLowerCase().startsWith("p")?"project":"user"}async function re(e){if(e.endpoint)return e.endpoint;let o=process.env.HM_ENDPOINT||process.env.MEMORYAI_ENDPOINT;return o||(k()?W:await $("Endpoint",W))}async function se(e,o){if(e.key)return e.key;let t=process.env.HM_API_KEY||process.env.MEMORYAI_API_KEY;if(t)return t;let n="";if(k()||(n=(await $("API key (leave blank to auto-create a free one")).trim()),!n){process.stdout.write(" ... creating a free key for you ...");let r=await V(o,"claude-code");if(r)return console.log(` ok (plan=${r.plan||"free"})`),r.api_key;throw console.log(" failed"),new Error("Could not create a key automatically. Get one from https://memoryai.dev/connect and rerun with --key.")}return n}function ie(e){try{let o=new URL(e);return`${o.origin}${o.pathname.replace("/v1/hooks/claude/","/v1/hooks/claude/")}`}catch{return e}}async function ae(e){h();let o=await ee(e),t=await re(e),n=await se(e,t);if(process.stdout.write(" ... verifying key ..."),await G(t,n))console.log(" ok");else if(console.log(" failed"),!e.yes&&!k()&&!await R("Server did not accept the key (or is offline). Continue anyway?",!1))throw new Error("Aborted by user.");let s=v(o),c=N(o),f=w(s),u=M(s);u&&console.log(` back ${u}`),(0,a.mkdirSync)(j,{recursive:!0}),(0,a.writeFileSync)(E,JSON.stringify({endpoint:t,apiKey:n},null,2)+`
|
|
27
|
+
`,"utf-8");try{require("node:fs").chmodSync(E,384)}catch{}let y=ce();if(!y)throw new Error("session-start-runner.js not found alongside the CLI. If running from source, run `npm run build` first.");(0,a.copyFileSync)(y,_),console.log(` setup ${_}`);let i=process.execPath,d=t.replace(/\/+$/,""),b={SessionStart:A(f,"SessionStart",K(i,_,12)),UserPromptSubmit:A(f,"UserPromptSubmit",H(`${d}/v1/hooks/claude/user-prompt`,n,10)),Stop:A(f,"Stop",H(`${d}/v1/hooks/claude/stop`,n,15))};for(let[T,U]of Object.entries(b))console.log(` ${U?"add ":"skip "} hook ${T}${U?"":" (already present)"}`);I(s,f),console.log(` write ${s}`);let C=z(c);console.log(C==="created"?` create ${c}`:C==="appended"?` append ${c}`:` skip ${c} (note already present)`),console.log(""),console.log("Installed. Restart Claude Code once, then just work."),console.log(" - Past context returns at the start of each prompt."),console.log(" - Important moments save when each turn ends."),console.log(" - Run `memoryai-claude doctor` any time to verify health.")}function ce(){let e=[(0,m.join)((0,m.dirname)(process.argv[1]||""),"session-start-runner.js"),(0,m.join)(__dirname,"session-start-runner.js")];for(let o of e)if(o&&(0,a.existsSync)(o))return o;return null}async function le(){h(),console.log("Diagnostics:");let e=v("user"),o=v("project"),t=!1;for(let[n,r]of[["user",e],["project",o]]){if(!(0,a.existsSync)(r)){console.log(` -- ${n}: ${r} (not present)`);continue}let s=w(r),c=O(s);if(Object.values(c).filter(i=>i.present).length===0){console.log(` -- ${n}: ${r} (no MemoryAI hooks)`);continue}t=!0,console.log(` ok ${n}: ${r}`);for(let[i,d]of Object.entries(c)){let b=d.url?` ${ie(d.url)}`:d.command?` command: ${d.command}`:"";console.log(` ${d.present?"present":"MISSING"} ${i}${b}`)}let u="",y="";if((0,a.existsSync)(E))try{let i=JSON.parse((0,a.readFileSync)(E,"utf-8"));i&&typeof i.endpoint=="string"&&(u=i.endpoint),i&&typeof i.apiKey=="string"&&(y=i.apiKey)}catch{}if(!u||!y){let i=ue(s);i?.url&&(u=new URL(i.url).origin,y=(i.headers?.Authorization||"").replace(/^Bearer\s+/i,""))}if(u&&y){process.stdout.write(" ... ping endpoints ...");let i=await q(u,y);console.log("");for(let[d,b]of Object.entries(i))console.log(` ${b?"ok":"FAIL"} ${d}`)}}t||(console.log(""),console.log("No MemoryAI hooks found in either user or project settings."),console.log("Run `memoryai-claude install` to wire them up."))}function ue(e){let o=e?.hooks||{};for(let t of Object.keys(o))for(let n of o[t]||[])for(let r of n.hooks||[])if(typeof r?.url=="string"&&r.url.includes("/v1/hooks/claude/"))return r;return null}async function de(){let e=(0,m.join)(j,"runner.log");if(!(0,a.existsSync)(e)){h(),console.log(`No SessionStart hook activity yet (${e} does not exist).`),console.log("This means Claude Code has not fired SessionStart since install."),console.log("Open a fresh terminal and run `claude` to trigger it.");return}let n=(0,a.readFileSync)(e,"utf-8").trim().split(/\r?\n/).slice(-20).join(`
|
|
28
|
+
`);h(),console.log(`SessionStart runner log (${e}):`),console.log(""),console.log(n)}async function pe(){h();for(let e of["user","project"]){let o=v(e);if(!(0,a.existsSync)(o)){console.log(` ${e}: not present`);continue}let t=w(o),n=O(t),r=Object.values(n).filter(s=>s.present).length;console.log(` ${e}: ${r}/3 hooks wired (${o})`)}}async function me(e){h();let o=await ee(e),t=v(o),n=N(o);if(!(0,a.existsSync)(t)){console.log(` Nothing to do \u2014 ${t} does not exist.`);return}if(!e.yes&&!k()&&!await R(`Remove MemoryAI hooks from ${t}?`,!0))return;let r=w(t),s=M(t);s&&console.log(` back ${s}`);let c=L(r);I(t,r),console.log(` removed ${c} hook${c===1?"":"s"} from ${t}`),B(n)&&console.log(` cleaned ${n}`),console.log(""),console.log("Uninstalled. Restart Claude Code once. Memory still lives on the server until you delete it.")}async function fe(){let e=ne(process.argv.slice(2));try{switch(e.command){case"install":await ae(e);break;case"doctor":await le();break;case"status":await pe();break;case"logs":await de();break;case"uninstall":await me(e);break;case"-v":case"--version":console.log(Z);break;case"help":case"--help":case"-h":case void 0:case"":Q();break;default:console.error(`Unknown command: ${e.command}`),Q(),process.exit(2)}}catch(o){console.error(""),console.error(`Error: ${o.message}`),process.exit(1)}}fe();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";var o=require("node:fs"),s=require("node:path"),p=require("node:os"),d=process.env.MEMORYAI_CLAUDE_CONFIG||(0,s.join)((0,p.homedir)(),".memoryai","claude.json"),f=(0,s.join)((0,p.homedir)(),".memoryai","runner.log");function n(t){try{(0,o.mkdirSync)((0,s.dirname)(f),{recursive:!0}),(0,o.appendFileSync)(f,`${new Date().toISOString()} ${t}
|
|
3
|
+
`,"utf-8")}catch{}}function l(){if(!(0,o.existsSync)(d))return n("config-missing"),null;try{let t=JSON.parse((0,o.readFileSync)(d,"utf-8"));return t&&typeof t.endpoint=="string"&&typeof t.apiKey=="string"?t:(n("config-malformed"),null)}catch(t){return n(`config-parse-error: ${t.message}`),null}}async function y(){return new Promise(t=>{let e="";if(process.stdin.isTTY){t({});return}process.stdin.setEncoding("utf-8"),process.stdin.on("data",r=>{e+=r}),process.stdin.on("end",()=>{try{t(e?JSON.parse(e):{})}catch{t({})}}),setTimeout(()=>t(e?S(e):{}),800)})}function S(t){try{return JSON.parse(t)}catch{return{}}}function h(t){let r={hookSpecificOutput:{hookEventName:"SessionStart",additionalContext:t.slice(0,9500)}};process.stdout.write(JSON.stringify(r))}async function k(){let e=(await y())?.source||"unknown";n(`fired source=${e}`);let r=l();r||process.exit(0);try{let a=r.endpoint.replace(/\/+$/,""),u=new AbortController,g=setTimeout(()=>u.abort(),1e4),c=await fetch(`${a}/v1/ide/guard/bootstrap`,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${r.apiKey}`},body:JSON.stringify({task:"",limit:14}),signal:u.signal});clearTimeout(g),c.ok||(n(`bootstrap-http-${c.status}`),process.exit(0));let i=await c.json(),m=i&&typeof i.context_block=="string"?i.context_block.trim():"";m||(n("bootstrap-empty"),process.exit(0)),h(m),n(`bootstrap-ok memories=${i.memories_restored??"?"} tokens=${i.tokens_used??"?"}`)}catch(a){n(`exception: ${a.message}`)}process.exit(0)}k();
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "memoryai-claude",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Your AI keeps forgetting you. MemoryAI gives Claude Code a real long-term memory — one that follows you across every model.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://memoryai.dev",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/memoryai-dev/memoryai-claude"
|
|
10
|
+
},
|
|
11
|
+
"keywords": [
|
|
12
|
+
"claude-code",
|
|
13
|
+
"memory",
|
|
14
|
+
"context",
|
|
15
|
+
"llm",
|
|
16
|
+
"ai"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=18"
|
|
20
|
+
},
|
|
21
|
+
"bin": {
|
|
22
|
+
"memoryai-claude": "dist/cli.js"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist/",
|
|
26
|
+
"README.md",
|
|
27
|
+
"LICENSE"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"vscode:prepublish": "npm run package",
|
|
31
|
+
"build:cli": "esbuild ./src/cli.ts --bundle --outfile=dist/cli.js --platform=node --target=node18 --format=cjs --banner:js=\"#!/usr/bin/env node\"",
|
|
32
|
+
"build:runner": "esbuild ./src/session-start-runner.ts --bundle --outfile=dist/session-start-runner.js --platform=node --target=node18 --format=cjs --banner:js=\"#!/usr/bin/env node\"",
|
|
33
|
+
"build": "npm run build:cli && npm run build:runner",
|
|
34
|
+
"package:cli": "npm run build:cli -- --minify",
|
|
35
|
+
"package:runner": "npm run build:runner -- --minify",
|
|
36
|
+
"package": "npm run package:cli && npm run package:runner",
|
|
37
|
+
"lint": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^22.0.0",
|
|
41
|
+
"esbuild": "^0.25.0",
|
|
42
|
+
"typescript": "^5.5.0"
|
|
43
|
+
}
|
|
44
|
+
}
|