@residue/cli 0.0.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/dist/residue ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@residue/cli",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "residue": "./dist/index.js"
7
+ },
8
+ "scripts": {
9
+ "dev": "bun run src/index.ts",
10
+ "build": "bun build src/index.ts --outfile dist/index.js --target bun",
11
+ "test": "bun test",
12
+ "typecheck": "tsc --noEmit"
13
+ },
14
+ "devDependencies": {
15
+ "@types/bun": "latest",
16
+ "@types/debug": "^4.1.12",
17
+ "typescript": "^5.7.0"
18
+ },
19
+ "dependencies": {
20
+ "commander": "^14.0.3",
21
+ "debug": "^4.4.3",
22
+ "neverthrow": "^8.2.0"
23
+ }
24
+ }
@@ -0,0 +1,49 @@
1
+ import { ok, ResultAsync, safeTry } from "neverthrow";
2
+ import { getCurrentBranch, getCurrentSha } from "@/lib/git";
3
+ import type { PendingSession } from "@/lib/pending";
4
+ import {
5
+ getPendingPath,
6
+ getProjectRoot,
7
+ readPending,
8
+ writePending,
9
+ } from "@/lib/pending";
10
+ import type { CliError } from "@/utils/errors";
11
+
12
+ /**
13
+ * Determine whether a session should be tagged with the current commit.
14
+ *
15
+ * - Open sessions: always tag (the agent is still active, the commit
16
+ * may contain code produced by this session).
17
+ * - Ended sessions with zero commits: tag once (the session ended before
18
+ * any capture ran, so the current commit is likely informed by it).
19
+ * - Ended sessions that already have commits: skip (they were already
20
+ * captured and are just waiting for sync -- tagging them with every
21
+ * subsequent commit would incorrectly link unrelated work).
22
+ */
23
+ function shouldTag(session: PendingSession): boolean {
24
+ if (session.status === "open") return true;
25
+ return session.commits.length === 0;
26
+ }
27
+
28
+ export function capture(): ResultAsync<void, CliError> {
29
+ return safeTry(async function* () {
30
+ const [sha, branch] = yield* ResultAsync.combine([
31
+ getCurrentSha(),
32
+ getCurrentBranch(),
33
+ ]);
34
+ const projectRoot = yield* getProjectRoot();
35
+ const pendingPath = yield* getPendingPath(projectRoot);
36
+ const sessions = yield* readPending(pendingPath);
37
+
38
+ for (const session of sessions) {
39
+ if (!shouldTag(session)) continue;
40
+ const isAlreadyTagged = session.commits.some((c) => c.sha === sha);
41
+ if (!isAlreadyTagged) {
42
+ session.commits.push({ sha, branch });
43
+ }
44
+ }
45
+
46
+ yield* writePending({ path: pendingPath, sessions });
47
+ return ok(undefined);
48
+ });
49
+ }
@@ -0,0 +1,222 @@
1
+ import { ok, okAsync, Result, ResultAsync, safeTry } from "neverthrow";
2
+ import {
3
+ addSession,
4
+ getPendingPath,
5
+ getProjectRoot,
6
+ getSession,
7
+ updateSession,
8
+ } from "@/lib/pending";
9
+ import { type CliError, toCliError } from "@/utils/errors";
10
+ import { createLogger } from "@/utils/logger";
11
+
12
+ const log = createLogger("hook");
13
+
14
+ import { mkdir, readFile, rm, stat, writeFile } from "fs/promises";
15
+ import { join } from "path";
16
+
17
+ type ClaudeHookInput = {
18
+ session_id: string;
19
+ transcript_path?: string;
20
+ cwd?: string;
21
+ hook_event_name: string;
22
+ source?: string;
23
+ [key: string]: unknown;
24
+ };
25
+
26
+ function readStdin(): ResultAsync<string, CliError> {
27
+ return ResultAsync.fromPromise(
28
+ (async () => {
29
+ const chunks: Uint8Array[] = [];
30
+ const reader = Bun.stdin.stream().getReader();
31
+ while (true) {
32
+ const { done, value } = await reader.read();
33
+ if (done) break;
34
+ chunks.push(value);
35
+ }
36
+ return Buffer.concat(chunks).toString("utf-8");
37
+ })(),
38
+ toCliError({ message: "Failed to read stdin", code: "IO_ERROR" }),
39
+ );
40
+ }
41
+
42
+ function parseInput(raw: string): Result<ClaudeHookInput, CliError> {
43
+ return Result.fromThrowable(
44
+ (input: string) => JSON.parse(input) as ClaudeHookInput,
45
+ toCliError({
46
+ message: "Failed to parse hook input JSON",
47
+ code: "PARSE_ERROR",
48
+ }),
49
+ )(raw);
50
+ }
51
+
52
+ function detectClaudeVersion(): ResultAsync<string, CliError> {
53
+ return ResultAsync.fromPromise(
54
+ (async () => {
55
+ const proc = Bun.spawn(["claude", "--version"], {
56
+ stdout: "pipe",
57
+ stderr: "pipe",
58
+ });
59
+ const exitCode = await proc.exited;
60
+ if (exitCode !== 0) return "unknown";
61
+ const output = await new Response(proc.stdout).text();
62
+ return output.trim() || "unknown";
63
+ })(),
64
+ toCliError({
65
+ message: "Failed to detect Claude version",
66
+ code: "IO_ERROR",
67
+ }),
68
+ ).orElse(() => okAsync("unknown"));
69
+ }
70
+
71
+ function getHooksDir(projectRoot: string): ResultAsync<string, CliError> {
72
+ const hooksDir = join(projectRoot, ".residue", "hooks");
73
+ return ResultAsync.fromPromise(
74
+ (async () => {
75
+ await mkdir(hooksDir, { recursive: true });
76
+ return hooksDir;
77
+ })(),
78
+ toCliError({
79
+ message: "Failed to create hooks state directory",
80
+ code: "IO_ERROR",
81
+ }),
82
+ );
83
+ }
84
+
85
+ function fileExists(path: string): ResultAsync<boolean, CliError> {
86
+ return ResultAsync.fromPromise(
87
+ (async () => {
88
+ try {
89
+ await stat(path);
90
+ return true;
91
+ } catch {
92
+ return false;
93
+ }
94
+ })(),
95
+ toCliError({
96
+ message: "Failed to check file existence",
97
+ code: "IO_ERROR",
98
+ }),
99
+ );
100
+ }
101
+
102
+ function readFileContent(path: string): ResultAsync<string, CliError> {
103
+ return ResultAsync.fromPromise(
104
+ readFile(path, "utf-8"),
105
+ toCliError({ message: "Failed to read file", code: "IO_ERROR" }),
106
+ );
107
+ }
108
+
109
+ function handleSessionStart(opts: {
110
+ input: ClaudeHookInput;
111
+ projectRoot: string;
112
+ }): ResultAsync<void, CliError> {
113
+ const { input, projectRoot } = opts;
114
+
115
+ if (input.source !== "startup") return okAsync(undefined);
116
+ if (!input.transcript_path) return okAsync(undefined);
117
+ if (!input.session_id) return okAsync(undefined);
118
+
119
+ const residueSessionId = crypto.randomUUID();
120
+ const claudeSessionId = input.session_id;
121
+
122
+ return safeTry(async function* () {
123
+ const pendingPath = yield* getPendingPath(projectRoot);
124
+
125
+ yield* addSession({
126
+ path: pendingPath,
127
+ session: {
128
+ id: residueSessionId,
129
+ agent: "claude-code",
130
+ agent_version: "unknown",
131
+ status: "open",
132
+ data_path: input.transcript_path!,
133
+ commits: [],
134
+ },
135
+ });
136
+
137
+ const version = yield* detectClaudeVersion();
138
+
139
+ yield* updateSession({
140
+ path: pendingPath,
141
+ id: residueSessionId,
142
+ updates: { agent_version: version },
143
+ });
144
+
145
+ const hooksDir = yield* getHooksDir(projectRoot);
146
+ const stateFile = join(hooksDir, `${claudeSessionId}.state`);
147
+
148
+ yield* ResultAsync.fromPromise(
149
+ writeFile(stateFile, residueSessionId),
150
+ toCliError({
151
+ message: "Failed to write hook state file",
152
+ code: "IO_ERROR",
153
+ }),
154
+ );
155
+
156
+ log.debug("session started for claude-code");
157
+ return ok(undefined);
158
+ });
159
+ }
160
+
161
+ function handleSessionEnd(opts: {
162
+ input: ClaudeHookInput;
163
+ projectRoot: string;
164
+ }): ResultAsync<void, CliError> {
165
+ const { input, projectRoot } = opts;
166
+ const claudeSessionId = input.session_id;
167
+
168
+ if (!claudeSessionId) return okAsync(undefined);
169
+
170
+ return safeTry(async function* () {
171
+ const hooksDir = yield* getHooksDir(projectRoot);
172
+ const stateFile = join(hooksDir, `${claudeSessionId}.state`);
173
+
174
+ const isExists = yield* fileExists(stateFile);
175
+ if (!isExists) return ok(undefined);
176
+
177
+ const rawId = yield* readFileContent(stateFile);
178
+ const trimmedId = rawId.trim();
179
+ if (!trimmedId) return ok(undefined);
180
+
181
+ const pendingPath = yield* getPendingPath(projectRoot);
182
+ const session = yield* getSession({ path: pendingPath, id: trimmedId });
183
+
184
+ if (session) {
185
+ yield* updateSession({
186
+ path: pendingPath,
187
+ id: trimmedId,
188
+ updates: { status: "ended" },
189
+ });
190
+ }
191
+
192
+ yield* ResultAsync.fromPromise(
193
+ rm(stateFile, { force: true }),
194
+ toCliError({
195
+ message: "Failed to remove hook state file",
196
+ code: "IO_ERROR",
197
+ }),
198
+ );
199
+
200
+ log.debug("session %s ended", trimmedId);
201
+ return ok(undefined);
202
+ });
203
+ }
204
+
205
+ export function hookClaudeCode(): ResultAsync<void, CliError> {
206
+ return safeTry(async function* () {
207
+ const raw = yield* readStdin();
208
+ const input = yield* parseInput(raw);
209
+ const projectRoot = yield* getProjectRoot();
210
+
211
+ switch (input.hook_event_name) {
212
+ case "SessionStart":
213
+ yield* handleSessionStart({ input, projectRoot });
214
+ break;
215
+ case "SessionEnd":
216
+ yield* handleSessionEnd({ input, projectRoot });
217
+ break;
218
+ }
219
+
220
+ return ok(undefined);
221
+ });
222
+ }
@@ -0,0 +1,146 @@
1
+ import { err, ok, ResultAsync, safeTry } from "neverthrow";
2
+ import { isGitRepo } from "@/lib/git";
3
+ import { getProjectRoot, getResidueDir } from "@/lib/pending";
4
+ import { CliError, toCliError } from "@/utils/errors";
5
+ import { createLogger } from "@/utils/logger";
6
+
7
+ const log = createLogger("init");
8
+
9
+ import {
10
+ appendFile,
11
+ chmod,
12
+ mkdir,
13
+ readFile,
14
+ stat,
15
+ writeFile,
16
+ } from "fs/promises";
17
+ import { join } from "path";
18
+
19
+ const POST_COMMIT_LINE = "residue capture >/dev/null 2>&1 &";
20
+ const PRE_PUSH_LINE = 'residue sync --remote-url "$2"';
21
+
22
+ function installHook(opts: {
23
+ hooksDir: string;
24
+ filename: string;
25
+ line: string;
26
+ }): ResultAsync<string, CliError> {
27
+ const hookPath = join(opts.hooksDir, opts.filename);
28
+
29
+ return ResultAsync.fromPromise(
30
+ (async () => {
31
+ let isExisting = false;
32
+ try {
33
+ await stat(hookPath);
34
+ isExisting = true;
35
+ } catch {
36
+ // file does not exist
37
+ }
38
+
39
+ if (isExisting) {
40
+ const content = await readFile(hookPath, "utf-8");
41
+ if (content.includes(opts.line)) {
42
+ return `${opts.filename}: already installed`;
43
+ }
44
+ await writeFile(hookPath, content.trimEnd() + "\n" + opts.line + "\n");
45
+ await chmod(hookPath, 0o755);
46
+ return `${opts.filename}: appended`;
47
+ }
48
+
49
+ await writeFile(hookPath, `#!/bin/sh\n${opts.line}\n`);
50
+ await chmod(hookPath, 0o755);
51
+ return `${opts.filename}: created`;
52
+ })(),
53
+ toCliError({
54
+ message: `Failed to install hook ${opts.filename}`,
55
+ code: "IO_ERROR",
56
+ }),
57
+ );
58
+ }
59
+
60
+ function getGitDirForInit(): ResultAsync<string, CliError> {
61
+ return ResultAsync.fromPromise(
62
+ (async () => {
63
+ const proc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
64
+ stdout: "pipe",
65
+ stderr: "pipe",
66
+ });
67
+ await proc.exited;
68
+ return (await new Response(proc.stdout).text()).trim();
69
+ })(),
70
+ toCliError({ message: "Failed to get git directory", code: "GIT_ERROR" }),
71
+ );
72
+ }
73
+
74
+ function ensureGitignore(projectRoot: string): ResultAsync<void, CliError> {
75
+ const gitignorePath = join(projectRoot, ".gitignore");
76
+
77
+ return ResultAsync.fromPromise(
78
+ (async () => {
79
+ let content = "";
80
+ try {
81
+ content = await readFile(gitignorePath, "utf-8");
82
+ } catch {
83
+ // file does not exist yet
84
+ }
85
+
86
+ if (content.includes(".residue")) {
87
+ return;
88
+ }
89
+
90
+ const line =
91
+ content.length > 0 && !content.endsWith("\n")
92
+ ? "\n.residue/\n"
93
+ : ".residue/\n";
94
+ await appendFile(gitignorePath, line);
95
+ })(),
96
+ toCliError({ message: "Failed to update .gitignore", code: "IO_ERROR" }),
97
+ );
98
+ }
99
+
100
+ export function init(): ResultAsync<void, CliError> {
101
+ return safeTry(async function* () {
102
+ const isRepo = yield* isGitRepo();
103
+ if (!isRepo) {
104
+ return err(
105
+ new CliError({ message: "not a git repository", code: "GIT_ERROR" }),
106
+ );
107
+ }
108
+
109
+ const [projectRoot, gitDir] = yield* ResultAsync.combine([
110
+ getProjectRoot(),
111
+ getGitDirForInit(),
112
+ ]);
113
+
114
+ const hooksDir = join(gitDir, "hooks");
115
+
116
+ yield* ResultAsync.combine([
117
+ getResidueDir(projectRoot),
118
+ ResultAsync.fromPromise(
119
+ mkdir(hooksDir, { recursive: true }),
120
+ toCliError({
121
+ message: "Failed to create hooks directory",
122
+ code: "IO_ERROR",
123
+ }),
124
+ ),
125
+ ]);
126
+
127
+ const [postCommit, prePush] = yield* ResultAsync.combine([
128
+ installHook({
129
+ hooksDir,
130
+ filename: "post-commit",
131
+ line: POST_COMMIT_LINE,
132
+ }),
133
+ installHook({
134
+ hooksDir,
135
+ filename: "pre-push",
136
+ line: PRE_PUSH_LINE,
137
+ }),
138
+ ensureGitignore(projectRoot),
139
+ ]);
140
+
141
+ log.info("Initialized residue in this repository.");
142
+ log.info(` ${postCommit}`);
143
+ log.info(` ${prePush}`);
144
+ return ok(undefined);
145
+ });
146
+ }
@@ -0,0 +1,26 @@
1
+ import { errAsync, type ResultAsync } from "neverthrow";
2
+ import { writeConfig } from "@/lib/config";
3
+ import { CliError } from "@/utils/errors";
4
+ import { createLogger } from "@/utils/logger";
5
+
6
+ const log = createLogger("login");
7
+
8
+ export function login(opts: {
9
+ url: string;
10
+ token: string;
11
+ }): ResultAsync<void, CliError> {
12
+ if (!opts.url.startsWith("http://") && !opts.url.startsWith("https://")) {
13
+ return errAsync(
14
+ new CliError({
15
+ message: "URL must start with http:// or https://",
16
+ code: "VALIDATION_ERROR",
17
+ }),
18
+ );
19
+ }
20
+
21
+ const cleanUrl = opts.url.replace(/\/+$/, "");
22
+
23
+ return writeConfig({ worker_url: cleanUrl, token: opts.token }).map(() => {
24
+ log.info(`Logged in to ${cleanUrl}`);
25
+ });
26
+ }
@@ -0,0 +1,3 @@
1
+ import { sync } from "@/commands/sync";
2
+
3
+ export const push = sync;
@@ -0,0 +1,38 @@
1
+ import type { ResultAsync } from "neverthrow";
2
+ import { err, ok, safeTry } from "neverthrow";
3
+ import {
4
+ getPendingPath,
5
+ getProjectRoot,
6
+ getSession,
7
+ updateSession,
8
+ } from "@/lib/pending";
9
+ import { CliError } from "@/utils/errors";
10
+ import { createLogger } from "@/utils/logger";
11
+
12
+ const log = createLogger("session");
13
+
14
+ export function sessionEnd(opts: { id: string }): ResultAsync<void, CliError> {
15
+ return safeTry(async function* () {
16
+ const projectRoot = yield* getProjectRoot();
17
+ const pendingPath = yield* getPendingPath(projectRoot);
18
+ const session = yield* getSession({ path: pendingPath, id: opts.id });
19
+
20
+ if (!session) {
21
+ return err(
22
+ new CliError({
23
+ message: `Session not found: ${opts.id}`,
24
+ code: "SESSION_NOT_FOUND",
25
+ }),
26
+ );
27
+ }
28
+
29
+ yield* updateSession({
30
+ path: pendingPath,
31
+ id: opts.id,
32
+ updates: { status: "ended" },
33
+ });
34
+
35
+ log.debug("session %s ended", opts.id);
36
+ return ok(undefined);
37
+ });
38
+ }
@@ -0,0 +1,35 @@
1
+ import type { ResultAsync } from "neverthrow";
2
+ import { addSession, getPendingPath, getProjectRoot } from "@/lib/pending";
3
+ import type { CliError } from "@/utils/errors";
4
+ import { createLogger } from "@/utils/logger";
5
+
6
+ const log = createLogger("session");
7
+
8
+ export function sessionStart(opts: {
9
+ agent: string;
10
+ data: string;
11
+ agentVersion: string;
12
+ }): ResultAsync<void, CliError> {
13
+ const id = crypto.randomUUID();
14
+
15
+ return getProjectRoot()
16
+ .andThen(getPendingPath)
17
+ .andThen((pendingPath) =>
18
+ addSession({
19
+ path: pendingPath,
20
+ session: {
21
+ id,
22
+ agent: opts.agent,
23
+ agent_version: opts.agentVersion,
24
+ status: "open",
25
+ data_path: opts.data,
26
+ commits: [],
27
+ },
28
+ }),
29
+ )
30
+ .map(() => {
31
+ // Only the session ID goes to stdout so adapters can capture it
32
+ process.stdout.write(id);
33
+ log.debug("session started for %s", opts.agent);
34
+ });
35
+ }
@@ -0,0 +1,148 @@
1
+ import { errAsync, okAsync, ResultAsync } from "neverthrow";
2
+ import { getProjectRoot } from "@/lib/pending";
3
+ import { CliError, toCliError } from "@/utils/errors";
4
+ import { createLogger } from "@/utils/logger";
5
+
6
+ const log = createLogger("setup");
7
+
8
+ import { mkdir, readFile, stat, writeFile } from "fs/promises";
9
+ import { join } from "path";
10
+ // Embedded at build time so the binary doesn't need to resolve a file path at runtime
11
+ import piAdapterSource from "../../adapters/pi/extension.ts.txt" with {
12
+ type: "text",
13
+ };
14
+
15
+ type HookHandler = {
16
+ type: string;
17
+ command: string;
18
+ timeout?: number;
19
+ };
20
+
21
+ type HookEntry = {
22
+ matcher: string;
23
+ hooks: HookHandler[];
24
+ };
25
+
26
+ type ClaudeSettings = {
27
+ hooks?: Record<string, HookEntry[]>;
28
+ [key: string]: unknown;
29
+ };
30
+
31
+ const CLAUDE_HOOK_COMMAND = "residue hook claude-code";
32
+
33
+ function hasResidueHook(entries: HookEntry[]): boolean {
34
+ return entries.some((entry) =>
35
+ entry.hooks.some((h) => h.command === CLAUDE_HOOK_COMMAND),
36
+ );
37
+ }
38
+
39
+ function setupClaudeCode(projectRoot: string): ResultAsync<void, CliError> {
40
+ const claudeDir = join(projectRoot, ".claude");
41
+ const settingsPath = join(claudeDir, "settings.json");
42
+
43
+ return ResultAsync.fromPromise(
44
+ (async () => {
45
+ await mkdir(claudeDir, { recursive: true });
46
+
47
+ let settings: ClaudeSettings = {};
48
+ try {
49
+ await stat(settingsPath);
50
+ const raw = await readFile(settingsPath, "utf-8");
51
+ settings = JSON.parse(raw) as ClaudeSettings;
52
+ } catch {
53
+ // file does not exist or is invalid
54
+ }
55
+
56
+ if (!settings.hooks) {
57
+ settings.hooks = {};
58
+ }
59
+
60
+ let isChanged = false;
61
+
62
+ // SessionStart hook
63
+ if (!settings.hooks.SessionStart) {
64
+ settings.hooks.SessionStart = [];
65
+ }
66
+ if (!hasResidueHook(settings.hooks.SessionStart)) {
67
+ settings.hooks.SessionStart.push({
68
+ matcher: "startup",
69
+ hooks: [
70
+ { type: "command", command: CLAUDE_HOOK_COMMAND, timeout: 10 },
71
+ ],
72
+ });
73
+ isChanged = true;
74
+ }
75
+
76
+ // SessionEnd hook
77
+ if (!settings.hooks.SessionEnd) {
78
+ settings.hooks.SessionEnd = [];
79
+ }
80
+ if (!hasResidueHook(settings.hooks.SessionEnd)) {
81
+ settings.hooks.SessionEnd.push({
82
+ matcher: "",
83
+ hooks: [
84
+ { type: "command", command: CLAUDE_HOOK_COMMAND, timeout: 10 },
85
+ ],
86
+ });
87
+ isChanged = true;
88
+ }
89
+
90
+ if (!isChanged) {
91
+ log.info("residue hooks already configured in .claude/settings.json");
92
+ return;
93
+ }
94
+
95
+ await writeFile(settingsPath, JSON.stringify(settings, null, 2) + "\n");
96
+ log.info("Configured Claude Code hooks in .claude/settings.json");
97
+ })(),
98
+ toCliError({ message: "Failed to setup Claude Code", code: "IO_ERROR" }),
99
+ );
100
+ }
101
+
102
+ function setupPi(projectRoot: string): ResultAsync<void, CliError> {
103
+ const extensionDir = join(projectRoot, ".pi", "extensions");
104
+ const targetPath = join(extensionDir, "residue.ts");
105
+
106
+ return ResultAsync.fromPromise(
107
+ (async () => {
108
+ await mkdir(extensionDir, { recursive: true });
109
+
110
+ let isExisting = false;
111
+ try {
112
+ await stat(targetPath);
113
+ isExisting = true;
114
+ } catch {
115
+ // does not exist
116
+ }
117
+
118
+ if (isExisting) {
119
+ log.info(
120
+ "residue extension already exists at .pi/extensions/residue.ts",
121
+ );
122
+ return;
123
+ }
124
+
125
+ await writeFile(targetPath, piAdapterSource);
126
+ log.info("Installed pi extension at .pi/extensions/residue.ts");
127
+ })(),
128
+ toCliError({ message: "Failed to setup pi", code: "IO_ERROR" }),
129
+ );
130
+ }
131
+
132
+ export function setup(opts: { agent: string }): ResultAsync<void, CliError> {
133
+ return getProjectRoot().andThen((projectRoot) => {
134
+ switch (opts.agent) {
135
+ case "claude-code":
136
+ return setupClaudeCode(projectRoot);
137
+ case "pi":
138
+ return setupPi(projectRoot);
139
+ default:
140
+ return errAsync(
141
+ new CliError({
142
+ message: `Unknown agent: ${opts.agent}. Supported: claude-code, pi`,
143
+ code: "VALIDATION_ERROR",
144
+ }),
145
+ );
146
+ }
147
+ });
148
+ }