@ariso-ai/ari-hooks 0.1.7 → 0.1.9

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
@@ -24,7 +24,11 @@ That single command:
24
24
 
25
25
  1. Opens your browser to log in to Ari and mint an API token (stored in
26
26
  `~/.ari-hooks/config.json`, `0600`). Only needed once per machine.
27
- 2. Adds three hooks to `./.claude/settings.json`:
27
+ 2. Detects which coding agents you use — Claude Code, Codex, and Cursor — and
28
+ wires up the hooks for each one it finds (Claude Code by `~/.claude`, Codex
29
+ by `~/.codex`, Cursor by the environment variables it sets in its terminal).
30
+ If it can't tell, it defaults to Claude Code.
31
+ 3. For Claude Code, adds three hooks to `./.claude/settings.json`:
28
32
  - `SessionStart` — fetches Ari's top suggested tasks and shows them when
29
33
  Claude Code boots; reply with a task number or name to run one
30
34
  - `UserPromptSubmit` — records what you asked for
@@ -33,6 +37,21 @@ That single command:
33
37
 
34
38
  Existing settings and hooks are preserved; running it again is a no-op.
35
39
 
40
+ ### Codex
41
+
42
+ Codex reads hooks from `.codex/hooks.json` (project) or `~/.codex/hooks.json`
43
+ (machine-wide), using the same nested shape as Claude Code. When `ari-hooks
44
+ install` (or `init`) detects Codex, it writes the two activity-sharing hooks
45
+ there:
46
+
47
+ - `UserPromptSubmit` — records what you asked for
48
+ - `Stop` — Codex hands the final assistant text to the hook directly (as
49
+ `last_assistant_message`), and that request/outcome pair is sent to the Ari
50
+ API
51
+
52
+ Codex has no user-visible channel to render Ari's suggested-task list, so the
53
+ `SessionStart` task prompt is skipped there.
54
+
36
55
  ### Cursor
37
56
 
38
57
  Cursor's agent doesn't read `.claude/settings.json` — it has its own hooks
@@ -48,14 +67,15 @@ writes the equivalent hooks there:
48
67
  this event instead)
49
68
  - `stop` — sends the request/outcome pair to the Ari API
50
69
 
51
- `ari-hooks uninstall` cleans up both files, wherever it runs.
70
+ `ari-hooks uninstall` cleans up every file — Claude Code, Codex, and Cursor —
71
+ wherever it runs.
52
72
 
53
73
  ### Commands
54
74
 
55
75
  | Command | What it does |
56
76
  |---|---|
57
- | `ari-hooks install` | Login (if needed) + set up hooks in the current folder |
58
- | `ari-hooks uninstall` | Remove the hooks from `./.claude/settings.json` and `./.cursor/hooks.json` |
77
+ | `ari-hooks install` | Login (if needed) + detect your agents and set up their hooks in the current folder |
78
+ | `ari-hooks uninstall` | Remove the hooks from the Claude Code, Codex (`./.codex/hooks.json`, `~/.codex/hooks.json`), and Cursor files |
59
79
  | `ari-hooks login` | Browser login, stores the API token |
60
80
  | `ari-hooks init` | Just add the hooks to `./.claude/settings.json` (no login) |
61
81
  | `ari-hooks config` | Show configured URLs and login state |
@@ -88,3 +108,7 @@ precedence over the stored config:
88
108
  logged to `~/.ari-hooks/error.log`.
89
109
  - Only the request text and the final assistant message are sent — no tool
90
110
  calls, diffs, or intermediate steps.
111
+
112
+ ## Author
113
+
114
+ Max Heckel
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ariso-ai/ari-hooks",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "description": "Set up Claude Code hooks that share your requests and their outcomes with Ari",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -6,13 +6,16 @@ import { loadConfig, setUrls, showConfig } from './config.js';
6
6
  const USAGE = `ari-hooks — share your Claude Code activity with Ari
7
7
 
8
8
  Usage:
9
- ari-hooks install Log in (if needed) and set up hooks; asks whether to
10
- install for just this repo (and then just for you —
11
- .claude/settings.local.json or everyone on the repo
12
- .claude/settings.json) or machine-wide
13
- (inside Cursor, also writes ./.cursor/hooks.json)
9
+ ari-hooks install Log in (if needed) and set up hooks. Detects which
10
+ coding agents you use (Claude Code, Codex, Cursor) and
11
+ wires up each one; asks whether to install for just this
12
+ repo (and then just for you — .claude/settings.local.json
13
+ or everyone on the repo — .claude/settings.json) or
14
+ machine-wide. Codex hooks go in ./.codex/hooks.json (or
15
+ ~/.codex/hooks.json), Cursor's in ./.cursor/hooks.json
14
16
  ari-hooks uninstall Remove the hooks from ./.claude/settings.json,
15
17
  ./.claude/settings.local.json, ~/.claude/settings.json,
18
+ ./.codex/hooks.json, ~/.codex/hooks.json,
16
19
  and ./.cursor/hooks.json
17
20
  ari-hooks login Log in via the browser and store an API token
18
21
  ari-hooks init Just add the hooks (no login)
@@ -37,6 +40,7 @@ function parseFlags(args) {
37
40
  if (args[i] === '--web-url') flags.webUrl = args[++i];
38
41
  else if (args[i] === '--api-url') flags.apiUrl = args[++i];
39
42
  else if (args[i] === '--reset-urls') flags.resetUrls = true;
43
+ else if (args[i] === '--agent') flags.agent = args[++i];
40
44
  else rest.push(args[i]);
41
45
  }
42
46
  return { flags, rest };
@@ -84,7 +88,7 @@ export async function main(argv) {
84
88
  uninstall();
85
89
  return;
86
90
  case 'hook':
87
- await runHook(rest[1]);
91
+ await runHook(rest[1], flags.agent);
88
92
  return;
89
93
  case undefined:
90
94
  // URL-only invocations (e.g. `ari-hooks --web-url ...`) are a
package/src/hooks.js CHANGED
@@ -66,6 +66,41 @@ const sessionIdOf = (input) => input.session_id ?? input.conversation_id;
66
66
  // Every Cursor hook payload carries cursor_version; Claude Code's never do.
67
67
  const isCursorInput = (input) => typeof input.cursor_version === 'string';
68
68
 
69
+ // agent_type values, keyed by the --agent slug the hook command passes (init
70
+ // bakes it into each agent's config file).
71
+ const AGENT_TYPES = { claude: 'claude-code', codex: 'codex', cursor: 'cursor' };
72
+
73
+ // Which coding agent produced this turn. The --agent flag from the hook command
74
+ // is the reliable source; fall back to sniffing the payload for installs that
75
+ // predate the flag — Cursor stamps cursor_version, Claude Code sends a
76
+ // transcript_path, and Codex has neither (it hands us last_assistant_message).
77
+ function agentTypeOf(input, agent) {
78
+ if (AGENT_TYPES[agent]) return AGENT_TYPES[agent];
79
+ if (isCursorInput(input)) return 'cursor';
80
+ if (input.transcript_path) return 'claude-code';
81
+ return 'codex';
82
+ }
83
+
84
+ // Claude Code auto-delivers a background task's completion as a synthetic
85
+ // UserPromptSubmit turn — the entire prompt is a <task-notification> block,
86
+ // not something the user typed. Recording it verbatim as a "request"
87
+ // pollutes the activity feed with agent-internal bookkeeping, so swap it for
88
+ // a short human-readable stand-in built from the notification's <summary>.
89
+ const TASK_NOTIFICATION_RE = /^<task-notification>[\s\S]*<\/task-notification>$/;
90
+ const SUMMARY_RE = /<summary>([\s\S]*?)<\/summary>/;
91
+ const XML_ENTITIES = { amp: '&', lt: '<', gt: '>', quot: '"', apos: "'" };
92
+ const decodeXmlEntities = (text) =>
93
+ text.replace(/&(amp|lt|gt|quot|apos);/g, (_, name) => XML_ENTITIES[name]);
94
+
95
+ function taskNotificationStandIn(prompt) {
96
+ const trimmed = prompt.trim();
97
+ if (!TASK_NOTIFICATION_RE.test(trimmed)) return null;
98
+ const summary = trimmed.match(SUMMARY_RE)?.[1]?.trim();
99
+ return summary
100
+ ? `The coding agent ran a background task: ${decodeXmlEntities(summary)}`
101
+ : 'The coding agent ran a background task.';
102
+ }
103
+
69
104
  /**
70
105
  * UserPromptSubmit (Claude Code) / beforeSubmitPrompt (Cursor): remember the
71
106
  * prompt so the Stop hook can pair it with the turn's outcome. Both hosts
@@ -75,7 +110,7 @@ async function onUserPromptSubmit(input) {
75
110
  const sessionId = sessionIdOf(input);
76
111
  if (!sessionId || typeof input.prompt !== 'string') return;
77
112
  const session = loadSession(sessionId);
78
- session.prompts.push(input.prompt);
113
+ session.prompts.push(taskNotificationStandIn(input.prompt) ?? input.prompt);
79
114
  saveSession(sessionId, session);
80
115
  }
81
116
 
@@ -158,7 +193,7 @@ const clamp = (text) =>
158
193
  * Stop: the turn is over — send the accumulated request(s) plus the final
159
194
  * assistant message to Ari, then clear the per-session state.
160
195
  */
161
- async function onStop(input) {
196
+ async function onStop(input, agent) {
162
197
  // stop_hook_active means a stop hook already forced Claude to continue;
163
198
  // the real end of the turn will fire another Stop event.
164
199
  if (input.stop_hook_active) return;
@@ -168,11 +203,12 @@ async function onStop(input) {
168
203
  const session = loadSession(sessionId);
169
204
  if (session.prompts.length === 0) return;
170
205
 
171
- // Cursor sessions get the outcome pushed to us via afterAgentResponse;
172
- // Claude Code sessions read it from the transcript, waiting for the final
173
- // assistant message to land there (on timeout, fall back to the last text
174
- // we did find best effort).
175
- let outcome = session.outcome ?? null;
206
+ // Cursor sessions get the outcome pushed to us via afterAgentResponse; Codex
207
+ // hands us the final text directly on the Stop payload as
208
+ // last_assistant_message; Claude Code sessions read it from the transcript,
209
+ // waiting for the final assistant message to land there (on timeout, fall
210
+ // back to the last text we did find — best effort).
211
+ let outcome = session.outcome ?? input.last_assistant_message ?? null;
176
212
  if (!outcome && input.transcript_path) {
177
213
  const deadline = Date.now() + OUTCOME_SETTLE_TIMEOUT_MS;
178
214
  for (;;) {
@@ -197,6 +233,7 @@ async function onStop(input) {
197
233
  request: clamp(session.prompts.join('\n\n')),
198
234
  outcome: clamp(outcome),
199
235
  session_id: sessionId,
236
+ agent_type: agentTypeOf(input, agent),
200
237
  // Cursor sends workspace_roots instead of cwd.
201
238
  cwd: input.cwd ?? input.workspace_roots?.[0] ?? process.cwd(),
202
239
  }),
@@ -317,7 +354,7 @@ async function onSessionStart(input) {
317
354
  * user's Claude Code session: all failures are swallowed (logged to
318
355
  * ~/.ari-hooks/error.log) and we always exit 0.
319
356
  */
320
- export async function runHook(event) {
357
+ export async function runHook(event, agent) {
321
358
  try {
322
359
  const raw = await readStdin();
323
360
  const input = raw ? JSON.parse(raw) : {};
@@ -326,7 +363,7 @@ export async function runHook(event) {
326
363
  } else if (event === 'agent-response') {
327
364
  await onAgentResponse(input);
328
365
  } else if (event === 'stop') {
329
- await onStop(input);
366
+ await onStop(input, agent);
330
367
  } else if (event === 'session-start') {
331
368
  await onSessionStart(input);
332
369
  }
package/src/init.js CHANGED
@@ -1,12 +1,28 @@
1
1
  import { dirname, join } from 'node:path';
2
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'node:fs';
3
3
  import { homedir } from 'node:os';
4
4
  import { createInterface } from 'node:readline/promises';
5
5
 
6
+ // Each command carries an --agent flag so the Stop hook can report which coding
7
+ // agent produced the activity (agent_name) without having to guess from the
8
+ // payload — the config file it lives in is the reliable source of that fact.
6
9
  const HOOK_EVENTS = {
7
- SessionStart: 'ari-hooks hook session-start',
8
- UserPromptSubmit: 'ari-hooks hook user-prompt-submit',
9
- Stop: 'ari-hooks hook stop',
10
+ SessionStart: 'ari-hooks hook session-start --agent claude',
11
+ UserPromptSubmit: 'ari-hooks hook user-prompt-submit --agent claude',
12
+ Stop: 'ari-hooks hook stop --agent claude',
13
+ };
14
+
15
+ // Codex copied Claude Code's hooks design: the same { hooks: { Event: [{ hooks:
16
+ // [{ type, command }] }] } } shape and the same event names, read from
17
+ // .codex/hooks.json (or ~/.codex/hooks.json). We only wire up the
18
+ // activity-sharing pair: UserPromptSubmit remembers the prompt and Stop ships
19
+ // the outcome (Codex hands us the final text on the Stop payload as
20
+ // last_assistant_message — no transcript to parse; see onStop). We skip
21
+ // SessionStart because Codex has no user-visible channel to render the
22
+ // suggested-task list, so there is nothing to show.
23
+ const CODEX_HOOK_EVENTS = {
24
+ UserPromptSubmit: 'ari-hooks hook user-prompt-submit --agent codex',
25
+ Stop: 'ari-hooks hook stop --agent codex',
10
26
  };
11
27
 
12
28
  // Cursor's agent doesn't read .claude/settings.json — it has its own hooks
@@ -14,10 +30,10 @@ const HOOK_EVENTS = {
14
30
  // format. Cursor's transcript is not the Claude Code JSONL our stop handler
15
31
  // parses, so afterAgentResponse captures the final assistant text instead.
16
32
  const CURSOR_HOOK_EVENTS = {
17
- sessionStart: 'ari-hooks hook session-start',
18
- beforeSubmitPrompt: 'ari-hooks hook user-prompt-submit',
19
- afterAgentResponse: 'ari-hooks hook agent-response',
20
- stop: 'ari-hooks hook stop',
33
+ sessionStart: 'ari-hooks hook session-start --agent cursor',
34
+ beforeSubmitPrompt: 'ari-hooks hook user-prompt-submit --agent cursor',
35
+ afterAgentResponse: 'ari-hooks hook agent-response --agent cursor',
36
+ stop: 'ari-hooks hook stop --agent cursor',
21
37
  };
22
38
 
23
39
  // Cursor's app install shows up in the env vars its integrated terminal
@@ -48,6 +64,37 @@ export const isCursor = (env = process.env) =>
48
64
  CURSOR_PATH_VARS.some((key) => /cursor/i.test(env[key] ?? ''))
49
65
  );
50
66
 
67
+ const dirExists = (path) => {
68
+ try {
69
+ return statSync(path).isDirectory();
70
+ } catch {
71
+ return false;
72
+ }
73
+ };
74
+
75
+ // Each agent keeps its config under a home directory whose presence is the
76
+ // tell that the user runs that agent; both honor the same env overrides the
77
+ // agents themselves do (CLAUDE_CONFIG_DIR, CODEX_HOME), which also lets tests
78
+ // point them at a scratch dir.
79
+ const claudeHome = (env) => env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude');
80
+ const codexHome = (env) => env.CODEX_HOME || join(homedir(), '.codex');
81
+
82
+ /**
83
+ * Which coding agents does this user run? Claude Code and Codex are detected by
84
+ * their config directory; Cursor by the marks it leaves in an integrated
85
+ * terminal (see isCursor). Returns a Set so install can branch to one config
86
+ * path per detected agent. Falls back to Claude Code when nothing is found so a
87
+ * fresh machine (or CI) still gets the primary target wired up.
88
+ */
89
+ export function detectAgents(env = process.env) {
90
+ const agents = new Set();
91
+ if (dirExists(claudeHome(env))) agents.add('claude');
92
+ if (dirExists(codexHome(env))) agents.add('codex');
93
+ if (isCursor(env)) agents.add('cursor');
94
+ if (agents.size === 0) agents.add('claude');
95
+ return agents;
96
+ }
97
+
51
98
  function readJson(path) {
52
99
  try {
53
100
  return JSON.parse(readFileSync(path, 'utf8'));
@@ -72,19 +119,36 @@ const writeJson = (path, value) =>
72
119
  */
73
120
  function claudeSettingsPath(scope, cwd, env) {
74
121
  if (scope === 'user') {
75
- return join(env.CLAUDE_CONFIG_DIR || join(homedir(), '.claude'), 'settings.json');
122
+ return join(claudeHome(env), 'settings.json');
76
123
  }
77
124
  const file = scope === 'local' ? 'settings.local.json' : 'settings.json';
78
125
  return join(cwd, '.claude', file);
79
126
  }
80
127
 
81
- function initClaude(settingsPath) {
128
+ // Codex has one project file and one user file — no committed/private split
129
+ // like Claude Code's settings.local.json — so 'local' collapses onto the
130
+ // project file.
131
+ function codexHooksPath(scope, cwd, env) {
132
+ if (scope === 'user') {
133
+ return join(codexHome(env), 'hooks.json');
134
+ }
135
+ return join(cwd, '.codex', 'hooks.json');
136
+ }
137
+
138
+ /**
139
+ * Merge the ari-hooks commands into a settings file that uses the shared
140
+ * Claude Code / Codex nested hook shape ({ hooks: { Event: [{ hooks: [...] }] }
141
+ * }). Idempotent: an event that already has an ari-hooks command is left alone,
142
+ * everything else in the file is preserved. `label` tags the success line so
143
+ * the user can tell which agent a file belongs to.
144
+ */
145
+ function initNestedHooks(settingsPath, events, label) {
82
146
  const settings = readJson(settingsPath) ?? {};
83
147
 
84
148
  settings.hooks ??= {};
85
149
  let changed = false;
86
150
 
87
- for (const [event, command] of Object.entries(HOOK_EVENTS)) {
151
+ for (const [event, command] of Object.entries(events)) {
88
152
  settings.hooks[event] ??= [];
89
153
  const already = settings.hooks[event].some((matcher) =>
90
154
  (matcher.hooks ?? []).some((h) => h.command?.includes('ari-hooks hook'))
@@ -103,10 +167,15 @@ function initClaude(settingsPath) {
103
167
 
104
168
  mkdirSync(dirname(settingsPath), { recursive: true });
105
169
  writeJson(settingsPath, settings);
106
- console.log(`✓ Ari hooks added to ${settingsPath}`);
170
+ console.log(`✓ Ari hooks added to ${settingsPath}${label ? ` (${label})` : ''}`);
107
171
  return true;
108
172
  }
109
173
 
174
+ const initClaude = (settingsPath) => initNestedHooks(settingsPath, HOOK_EVENTS);
175
+
176
+ const initCodex = (hooksPath) =>
177
+ initNestedHooks(hooksPath, CODEX_HOOK_EVENTS, 'Codex detected');
178
+
110
179
  function initCursor(cwd) {
111
180
  const cursorDir = join(cwd, '.cursor');
112
181
  const hooksPath = join(cursorDir, 'hooks.json');
@@ -168,17 +237,22 @@ async function chooseClaudeScope() {
168
237
  }
169
238
 
170
239
  /**
171
- * Merge the ari-hooks hook commands into Claude Code settings and, when
172
- * running inside Cursor, into .cursor/hooks.json as well. Idempotent:
173
- * existing ari-hooks entries are left alone, and unrelated hooks/settings
174
- * are preserved. `scope` picks the Claude settings file (see
175
- * claudeSettingsPath); the Cursor hooks file is always project-level.
240
+ * Merge the ari-hooks hook commands into the config of every coding agent this
241
+ * user runs (see detectAgents): Claude Code settings, Codex hooks.json, and
242
+ * inside Cursor — .cursor/hooks.json. Idempotent: existing ari-hooks entries
243
+ * are left alone, and unrelated hooks/settings are preserved. `scope` picks the
244
+ * Claude Code and Codex file (see claudeSettingsPath / codexHooksPath); the
245
+ * Cursor hooks file is always project-level.
176
246
  */
177
247
  export function init(cwd = process.cwd(), env = process.env, scope = 'project') {
178
- const changedClaude = initClaude(claudeSettingsPath(scope, cwd, env));
179
- const changedCursor = isCursor(env) ? initCursor(cwd) : false;
180
-
181
- if (!changedClaude && !changedCursor) return;
248
+ const agents = detectAgents(env);
249
+ const changed = [
250
+ agents.has('claude') && initClaude(claudeSettingsPath(scope, cwd, env)),
251
+ agents.has('codex') && initCodex(codexHooksPath(scope, cwd, env)),
252
+ agents.has('cursor') && initCursor(cwd),
253
+ ];
254
+
255
+ if (!changed.some(Boolean)) return;
182
256
  const where = scope === 'user' ? 'on this machine' : 'in this folder';
183
257
  console.log(
184
258
  `Agent sessions ${where} will now share each request and its outcome with Ari,`
@@ -187,11 +261,14 @@ export function init(cwd = process.cwd(), env = process.env, scope = 'project')
187
261
  }
188
262
 
189
263
  /**
190
- * The `install` flavor of init: when attached to a terminal, ask where the
191
- * Claude Code hooks should live before writing them. Non-interactive runs
192
- * (CI, piped stdin) keep the old default of ./.claude/settings.json.
264
+ * The `install` flavor of init: report which agents were detected, and when
265
+ * attached to a terminal — ask where the hooks should live before writing them.
266
+ * Non-interactive runs (CI, piped stdin) keep the old default of the
267
+ * project-level files.
193
268
  */
194
269
  export async function install(cwd = process.cwd(), env = process.env) {
270
+ const agents = detectAgents(env);
271
+ console.log(`Detected coding agent(s): ${[...agents].join(', ')}`);
195
272
  const scope =
196
273
  process.stdin.isTTY && process.stdout.isTTY
197
274
  ? await chooseClaudeScope()
@@ -199,7 +276,7 @@ export async function install(cwd = process.cwd(), env = process.env) {
199
276
  init(cwd, env, scope);
200
277
  }
201
278
 
202
- function uninstallClaude(settingsPath) {
279
+ function uninstallNestedHooks(settingsPath) {
203
280
  if (!existsSync(settingsPath)) return false;
204
281
  const settings = readJson(settingsPath);
205
282
 
@@ -256,20 +333,24 @@ function uninstallCursor(cwd) {
256
333
  }
257
334
 
258
335
  /**
259
- * Remove the ari-hooks hook commands that init/install added to the
260
- * Claude Code settings and Cursor hooks file. The inverse of init: only
261
- * ari-hooks entries are touched, everything else in the files is
262
- * preserved. Cleans every location install can write to (project
263
- * settings.json, settings.local.json, the user-level settings, and the
264
- * Cursor hooks file), so hooks don't linger wherever they were put.
336
+ * Remove the ari-hooks hook commands that init/install added to the Claude
337
+ * Code settings, Codex hooks.json, and Cursor hooks file. The inverse of init:
338
+ * only ari-hooks entries are touched, everything else in the files is
339
+ * preserved. Cleans every location install can write to — regardless of which
340
+ * agents are currently detected so hooks don't linger wherever they were put:
341
+ * the project/local/user Claude settings, the project and user Codex hooks, and
342
+ * the Cursor hooks file.
265
343
  */
266
344
  export function uninstall(cwd = process.cwd(), env = process.env) {
267
345
  const removedClaude = ['project', 'local', 'user']
268
- .map((scope) => uninstallClaude(claudeSettingsPath(scope, cwd, env)))
346
+ .map((scope) => uninstallNestedHooks(claudeSettingsPath(scope, cwd, env)))
347
+ .some(Boolean);
348
+ const removedCodex = ['project', 'user']
349
+ .map((scope) => uninstallNestedHooks(codexHooksPath(scope, cwd, env)))
269
350
  .some(Boolean);
270
351
  const removedCursor = uninstallCursor(cwd);
271
352
 
272
- if (!removedClaude && !removedCursor) {
353
+ if (!removedClaude && !removedCodex && !removedCursor) {
273
354
  console.log('No Ari hooks found in this folder — nothing to remove.');
274
355
  return;
275
356
  }