@reticlehq/server 3.8.2 → 3.9.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 (224) hide show
  1. package/LICENSE +201 -104
  2. package/NOTICE +5 -0
  3. package/README.md +1 -1
  4. package/bin/launch.js +13 -0
  5. package/bin/package.json +3 -0
  6. package/bin/reticle.js +66 -8
  7. package/dist/chromium-launch-options.d.ts +25 -0
  8. package/dist/chromium-launch-options.js +21 -1
  9. package/dist/command/cli/adhoc-verdict.js +34 -7
  10. package/dist/command/cli/answers/gate-exit.d.ts +6 -2
  11. package/dist/command/cli/answers/gate-exit.js +12 -2
  12. package/dist/command/cli/ci-hint.d.ts +44 -0
  13. package/dist/command/cli/ci-hint.js +72 -0
  14. package/dist/command/cli/cli-doctor.d.ts +6 -1
  15. package/dist/command/cli/cli-doctor.js +45 -4
  16. package/dist/command/cli/cli-flow-commands.d.ts +7 -1
  17. package/dist/command/cli/cli-flow-commands.js +39 -17
  18. package/dist/command/cli/cli-kill.d.ts +21 -0
  19. package/dist/command/cli/cli-kill.js +46 -3
  20. package/dist/command/cli/cli-parse.d.ts +5 -2
  21. package/dist/command/cli/cli-parse.js +10 -5
  22. package/dist/command/cli/cli-usage.js +3 -2
  23. package/dist/command/cli/cli-verify.d.ts +6 -1
  24. package/dist/command/cli/cli-verify.js +32 -13
  25. package/dist/command/cli/doctor/browser/chromium-hint.d.ts +6 -1
  26. package/dist/command/cli/doctor/browser/chromium-hint.js +11 -5
  27. package/dist/command/cli/doctor/doctor-fix.d.ts +44 -0
  28. package/dist/command/cli/doctor/doctor-fix.js +89 -0
  29. package/dist/command/cli/doctor/doctor-launcher-line.d.ts +23 -0
  30. package/dist/command/cli/doctor/doctor-launcher-line.js +60 -0
  31. package/dist/command/cli/doctor/doctor-rows.d.ts +4 -0
  32. package/dist/command/cli/doctor/doctor-rows.js +4 -0
  33. package/dist/command/cli/doctor-repairs.d.ts +64 -0
  34. package/dist/command/cli/doctor-repairs.js +214 -0
  35. package/dist/command/cli/drive/drive-command.js +3 -7
  36. package/dist/command/cli/first-use-checks.d.ts +41 -0
  37. package/dist/command/cli/first-use-checks.js +127 -0
  38. package/dist/command/cli/first-verdict-node.d.ts +3 -0
  39. package/dist/command/cli/first-verdict-node.js +189 -0
  40. package/dist/command/cli/first-verdict.d.ts +126 -0
  41. package/dist/command/cli/first-verdict.js +267 -0
  42. package/dist/command/cli/launch/sdk-version-match.d.ts +3 -2
  43. package/dist/command/cli/launch/sdk-version-match.js +3 -2
  44. package/dist/command/cli/lifecycle/daemon-lifecycle.js +6 -5
  45. package/dist/command/cli/mcp-command.js +13 -4
  46. package/dist/command/cli/ports/port-holder.d.ts +2 -0
  47. package/dist/command/cli/ports/port-holder.js +11 -3
  48. package/dist/command/cli/setup-mcp-cli.d.ts +27 -1
  49. package/dist/command/cli/setup-mcp-cli.js +98 -23
  50. package/dist/command/cli/status/status-lines.d.ts +1 -0
  51. package/dist/command/cli/status/status-lines.js +50 -17
  52. package/dist/command/cli/status-fields.js +17 -3
  53. package/dist/command/cli/try-command.d.ts +7 -0
  54. package/dist/command/cli/try-command.js +11 -3
  55. package/dist/command/cli.js +70 -33
  56. package/dist/command/daemon/binding/daemon-status-probe.d.ts +3 -1
  57. package/dist/command/daemon/binding/daemon-status-probe.js +4 -2
  58. package/dist/command/daemon/daemon-argv.d.ts +9 -0
  59. package/dist/command/daemon/daemon-argv.js +11 -0
  60. package/dist/command/daemon/daemon.d.ts +10 -3
  61. package/dist/command/daemon/daemon.js +17 -6
  62. package/dist/command/daemon/lifetime/daemon-exit.d.ts +24 -0
  63. package/dist/command/daemon/lifetime/daemon-exit.js +39 -0
  64. package/dist/command/daemon/lifetime/death-cause.d.ts +15 -0
  65. package/dist/command/daemon/lifetime/death-cause.js +130 -0
  66. package/dist/command/daemon/lifetime/heartbeat.d.ts +7 -15
  67. package/dist/command/daemon/lifetime/heartbeat.js +4 -13
  68. package/dist/command/daemon/port-state.d.ts +14 -0
  69. package/dist/command/daemon/port-state.js +31 -0
  70. package/dist/command/setup/agent-configs.d.ts +2 -0
  71. package/dist/command/setup/agent-configs.js +42 -15
  72. package/dist/command/setup/agent-next-steps.d.ts +77 -0
  73. package/dist/command/setup/agent-next-steps.js +284 -0
  74. package/dist/command/setup/agent-writer.js +11 -6
  75. package/dist/command/setup/init/init-host.js +2 -0
  76. package/dist/command/setup/init/init-runtime.d.ts +6 -1
  77. package/dist/command/setup/init/init-runtime.js +57 -21
  78. package/dist/command/setup/mcp-launcher.d.ts +3 -0
  79. package/dist/command/setup/mcp-launcher.js +11 -0
  80. package/dist/command/setup/setup-command.js +30 -12
  81. package/dist/command/setup/setup-mcp.d.ts +8 -0
  82. package/dist/command/setup/setup-mcp.js +47 -20
  83. package/dist/command/setup/start-dev-server.d.ts +8 -0
  84. package/dist/command/setup/start-dev-server.js +43 -0
  85. package/dist/command/version/identity/server-version.d.ts +2 -0
  86. package/dist/command/version/identity/server-version.js +2 -0
  87. package/dist/command/version/version-skew.d.ts +7 -0
  88. package/dist/command/version/version-skew.js +13 -2
  89. package/dist/features/license/license.d.ts +11 -71
  90. package/dist/features/license/license.js +43 -136
  91. package/dist/features/visual/desktop-capture.d.ts +33 -0
  92. package/dist/features/visual/desktop-capture.js +94 -0
  93. package/dist/features/visual/step-shots.js +139 -35
  94. package/dist/features/visual/visual-tools.d.ts +0 -18
  95. package/dist/features/visual/visual-tools.js +2 -81
  96. package/dist/import-graph.js +5 -6
  97. package/dist/index.d.ts +3 -1
  98. package/dist/index.js +14 -5
  99. package/dist/judgement/runs/run-ledger.d.ts +7 -5
  100. package/dist/judgement/runs/run-ledger.js +98 -69
  101. package/dist/judgement/runs/verification-sync.d.ts +6 -3
  102. package/dist/judgement/runs/verification-sync.js +21 -10
  103. package/dist/language/flows/flow-store-for-session.js +7 -5
  104. package/dist/language/flows/recording/tape/recordings.d.ts +2 -13
  105. package/dist/language/flows/recording/tape/recordings.js +41 -44
  106. package/dist/language/flows/replay.d.ts +17 -7
  107. package/dist/language/flows/replay.js +95 -44
  108. package/dist/launch-chromium.d.ts +1 -0
  109. package/dist/launch-chromium.js +49 -12
  110. package/dist/memory/cloud/sync-screenshots.js +6 -1
  111. package/dist/portal/bridge/bridge.d.ts +13 -4
  112. package/dist/portal/bridge/bridge.js +10 -9
  113. package/dist/portal/input/network-mock.js +3 -2
  114. package/dist/portal/input/real-input.d.ts +15 -0
  115. package/dist/portal/input/real-input.js +59 -3
  116. package/dist/portal/input/viewport-tools.js +6 -3
  117. package/dist/portal/pool/browser-pool.d.ts +5 -1
  118. package/dist/portal/pool/browser-pool.js +32 -38
  119. package/dist/portal/pool/context-permissions.d.ts +20 -17
  120. package/dist/portal/pool/context-permissions.js +63 -1
  121. package/dist/portal/pool/csp-widen.d.ts +62 -0
  122. package/dist/portal/pool/csp-widen.js +132 -0
  123. package/dist/portal/pool/lease-page-error.d.ts +29 -0
  124. package/dist/portal/pool/lease-page-error.js +18 -0
  125. package/dist/portal/pool/page-error-journal.d.ts +57 -0
  126. package/dist/portal/pool/page-error-journal.js +181 -0
  127. package/dist/portal/pool/playwright-launcher.js +21 -8
  128. package/dist/portal/pool/pool-contract.d.ts +22 -0
  129. package/dist/portal/pool/pool-lease.d.ts +35 -2
  130. package/dist/portal/pool/pool-lease.js +23 -0
  131. package/dist/portal/pool/zero-install.js +69 -1
  132. package/dist/portal/session/dev-server/dev-command.d.ts +8 -0
  133. package/dist/portal/session/dev-server/dev-command.js +56 -8
  134. package/dist/portal/session/no-session-diagnosis.js +10 -3
  135. package/dist/portal/session/no-session-next-action.d.ts +12 -0
  136. package/dist/portal/session/no-session-next-action.js +44 -4
  137. package/dist/portal/session/no-session-watch.js +3 -1
  138. package/dist/portal/session/page-commands/undeclared-command.d.ts +9 -3
  139. package/dist/portal/session/page-commands/undeclared-command.js +16 -7
  140. package/dist/portal/session/recorded-gaps.d.ts +1 -1
  141. package/dist/portal/session/recorded-gaps.js +5 -2
  142. package/dist/portal/session/session-manager.js +40 -21
  143. package/dist/portal/session/session.js +4 -3
  144. package/dist/portal/session/zero-install-first-call.d.ts +20 -1
  145. package/dist/portal/session/zero-install-first-call.js +34 -2
  146. package/dist/start-wiring.d.ts +29 -2
  147. package/dist/start-wiring.js +73 -9
  148. package/dist/surface/mcp/mcp-proxy.js +10 -2
  149. package/dist/surface/mcp/mcp.js +5 -3
  150. package/dist/surface/mcp/peer/agents-connected.d.ts +6 -0
  151. package/dist/surface/mcp/peer/agents-connected.js +58 -0
  152. package/dist/surface/mcp/proxy/proxy-daemon-probe.js +17 -5
  153. package/dist/surface/tools/act/act-preflight.js +37 -17
  154. package/dist/surface/tools/act/act-target.js +3 -1
  155. package/dist/surface/tools/act/after-match.d.ts +7 -0
  156. package/dist/surface/tools/act/after-match.js +33 -0
  157. package/dist/surface/tools/act/resolve-target.d.ts +2 -0
  158. package/dist/surface/tools/act/resolve-target.js +35 -8
  159. package/dist/surface/tools/act/sequence-grade.d.ts +3 -1
  160. package/dist/surface/tools/act/sequence-grade.js +8 -1
  161. package/dist/surface/tools/act-sequence-tool.js +3 -0
  162. package/dist/surface/tools/act-tools.js +10 -10
  163. package/dist/surface/tools/assert/assert-verdict.js +28 -1
  164. package/dist/surface/tools/browser-tools.js +75 -7
  165. package/dist/surface/tools/dynamic-tools.js +2 -1
  166. package/dist/surface/tools/empty-session-list.d.ts +2 -0
  167. package/dist/surface/tools/empty-session-list.js +4 -2
  168. package/dist/surface/tools/error-recovery.js +43 -22
  169. package/dist/surface/tools/invoke-tool.js +27 -20
  170. package/dist/surface/tools/lease-hint.d.ts +7 -0
  171. package/dist/surface/tools/lease-hint.js +10 -3
  172. package/dist/surface/tools/lease-readiness.d.ts +21 -0
  173. package/dist/surface/tools/lease-readiness.js +47 -0
  174. package/dist/surface/tools/lease-seed.d.ts +2 -0
  175. package/dist/surface/tools/lease-seed.js +2 -1
  176. package/dist/surface/tools/lease-tools.js +43 -15
  177. package/dist/surface/tools/repeat-hint.d.ts +40 -0
  178. package/dist/surface/tools/repeat-hint.js +171 -0
  179. package/dist/surface/tools/run-report.d.ts +2 -2
  180. package/dist/surface/tools/run-report.js +27 -15
  181. package/dist/surface/tools/tool-kit.d.ts +12 -0
  182. package/dist/surface/tools/tool-kit.js +7 -0
  183. package/dist/surface/tools/tools.js +26 -13
  184. package/dist/telemetry/argument-shape.js +16 -7
  185. package/dist/telemetry/cli-telemetry.d.ts +10 -1
  186. package/dist/telemetry/cli-telemetry.js +15 -0
  187. package/dist/telemetry/init-telemetry.d.ts +2 -1
  188. package/dist/telemetry/init-telemetry.js +47 -2
  189. package/dist/telemetry/install-trace.js +23 -4
  190. package/dist/telemetry/onboarding-funnel.d.ts +19 -2
  191. package/dist/telemetry/onboarding-funnel.js +25 -3
  192. package/dist/telemetry/proxy-telemetry.d.ts +2 -1
  193. package/dist/telemetry/proxy-telemetry.js +2 -2
  194. package/dist/telemetry/session-metrics.d.ts +1 -1
  195. package/dist/telemetry/session-metrics.js +67 -27
  196. package/dist/telemetry/telemetry.d.ts +3 -1
  197. package/dist/telemetry/telemetry.js +60 -28
  198. package/dist/telemetry/tool-refused.d.ts +2 -2
  199. package/dist/telemetry/tool-refused.js +9 -2
  200. package/docs/architecture.md +3 -6
  201. package/docs/cli/doctor.mdx +41 -2
  202. package/docs/cli/help.mdx +4 -2
  203. package/docs/cli/license.mdx +4 -5
  204. package/docs/cli/setup.mdx +13 -2
  205. package/docs/cli/status.mdx +4 -0
  206. package/docs/cli.mdx +1 -1
  207. package/docs/deploy-checks.md +25 -0
  208. package/docs/desktop.mdx +1 -1
  209. package/docs/enterprise.md +15 -20
  210. package/docs/environment.mdx +1 -0
  211. package/docs/first-drive.md +1 -1
  212. package/docs/install-manual.mdx +16 -1
  213. package/docs/packages/engine.mdx +1 -1
  214. package/docs/packages/server.mdx +1 -1
  215. package/docs/packages/tauri.mdx +1 -1
  216. package/docs/packages/test.mdx +1 -1
  217. package/docs/packages.mdx +1 -1
  218. package/docs/platform-integration.md +1 -1
  219. package/docs/skill.md +1 -1
  220. package/docs/telemetry.md +4 -2
  221. package/package.json +13 -9
  222. package/LICENSE-ENTERPRISE +0 -37
  223. package/dist/features/ee/audit-log.d.ts +0 -14
  224. package/dist/features/ee/audit-log.js +0 -11
@@ -0,0 +1,77 @@
1
+ import { McpClient } from '@reticlehq/init';
2
+ import { AgentClient } from '@reticlehq/core/telemetry';
3
+ import type { SetupMcpResult } from './setup-mcp.js';
4
+ /** What registration did for one client. */
5
+ export declare const AgentReg: {
6
+ /** The Claude Code plugin carries Reticle. */
7
+ readonly PLUGIN: "plugin";
8
+ readonly ADDED: "added";
9
+ readonly ALREADY: "already";
10
+ readonly FAILED: "failed";
11
+ readonly MANUAL: "manual";
12
+ /** Found, and Reticle is not in its config. Read-only callers (`status`) only. */
13
+ readonly MISSING: "missing";
14
+ };
15
+ export type AgentReg = (typeof AgentReg)[keyof typeof AgentReg];
16
+ export interface AgentRow {
17
+ readonly id: McpClient;
18
+ readonly reg: AgentReg;
19
+ /** The config path for a hand edit. */
20
+ readonly path?: string;
21
+ /** The command to retry a failed CLI registration, with the launcher that run used. */
22
+ readonly retry?: string;
23
+ /** The plugin AND a user-scope `reticle` entry: the client has two servers. */
24
+ readonly duplicate?: boolean;
25
+ /** The entry is the floating npx form: the note that names the commands that replace it. */
26
+ readonly stale?: string;
27
+ }
28
+ /**
29
+ * The rows for one `setupMcp` run, in its order.
30
+ *
31
+ * @param launcher the installed bin that run registered (`SetupMcpIo.launcher`), so a retry line
32
+ * names the same command.
33
+ */
34
+ export declare function agentRows(result: SetupMcpResult, launcher?: string): AgentRow[];
35
+ /** The "Your agents:" block: one line per client, each with its own next step. */
36
+ export declare function agentLines(rows: readonly AgentRow[]): string[];
37
+ /** The clients worth waiting for: the ones that have Reticle registered. */
38
+ export declare function awaitedClients(rows: readonly AgentRow[]): AgentClient[];
39
+ /** One agent, as `reticle status` reports it. */
40
+ export interface AgentStatus {
41
+ readonly name: string;
42
+ readonly connected: boolean;
43
+ /** The step still missing. Absent when connected. */
44
+ readonly next?: string;
45
+ /** The line a person reads: "Connected: Cursor ✓", or the name and the step still missing. */
46
+ readonly said: string;
47
+ }
48
+ /** Per client: connected, or the exact step still missing. */
49
+ export declare function agentStatuses(rows: readonly AgentRow[], connected: ReadonlySet<string>): AgentStatus[];
50
+ /** The lines the installer prints after its wait. */
51
+ export declare function connectionLines(rows: readonly AgentRow[], connected: ReadonlySet<string>): string[];
52
+ /** The clock the wait runs on. Injected, so a test does not wait. */
53
+ export interface WaitClock {
54
+ now(): number;
55
+ sleep(ms: number): Promise<void>;
56
+ }
57
+ /**
58
+ * Poll `read` until every client in `want` has connected, or `budgetMs` has passed. Returns the
59
+ * last read. Bounded: it never waits longer than the budget.
60
+ */
61
+ export declare function waitForAgents(want: readonly string[], read: () => ReadonlySet<string>, clock: WaitClock, budgetMs: number, pollMs: number): Promise<ReadonlySet<string>>;
62
+ /** `$CLAUDE_CONFIG_DIR`, else home: where Claude Code keeps `.claude.json`. */
63
+ export declare function claudeStateDir(): string;
64
+ /** What `installedAgentRows` reads. It never writes. */
65
+ export interface AgentProbe {
66
+ exists(path: string): boolean;
67
+ readFile(path: string): string | null;
68
+ homeDir(): string;
69
+ }
70
+ /**
71
+ * The agents on this machine and whether Reticle is in each config, read only. For `reticle status`,
72
+ * which must not register anything and must not spawn a client CLI.
73
+ *
74
+ * @param claudeStateDir the directory that holds `.claude.json`: `$CLAUDE_CONFIG_DIR`, else home.
75
+ * @param launcher the installed bin a new registration would use, for the stale-entry fix.
76
+ */
77
+ export declare function installedAgentRows(io: AgentProbe, projectDir: string | undefined, pluginActive: boolean, claudeStateDir?: string, launcher?: string): AgentRow[];
@@ -0,0 +1,284 @@
1
+ /**
2
+ * One next step for each coding agent, and whether each one really connected.
3
+ *
4
+ * Registration writes a config. An agent reads that config only at a time the client decides, so
5
+ * "registered" is not "the agent has the tools". This file names, per client, the one step that
6
+ * loads the tools, and reads which agents did connect (`agents-connected.ts`) to say so.
7
+ *
8
+ * Only the Claude Code plugin gets "no restart". Its docs say `/reload-plugins` loads it. For every
9
+ * other client the step is the conservative one: their docs do not say a running session picks up a
10
+ * new server.
11
+ */
12
+ import { join } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+ import { claudeAddCommand, claudeHasReticle, claudeRemoveUserCommand, claudeStaleEntryNote, claudeUserEntryIsFloating, clientSpec, codexNamesOurServer, ConfigFormat, detectMcpClients, McpClient, } from '@reticlehq/init';
15
+ import { MCP_SERVER_NAME } from '@reticlehq/core';
16
+ import { AgentClient } from '@reticlehq/core/telemetry';
17
+ /** What registration did for one client. */
18
+ export const AgentReg = {
19
+ /** The Claude Code plugin carries Reticle. */
20
+ PLUGIN: 'plugin',
21
+ ADDED: 'added',
22
+ ALREADY: 'already',
23
+ FAILED: 'failed',
24
+ MANUAL: 'manual',
25
+ /** Found, and Reticle is not in its config. Read-only callers (`status`) only. */
26
+ MISSING: 'missing',
27
+ };
28
+ /** The one table of client to next step. Change a step here, nowhere else. */
29
+ const AGENT_STEPS = {
30
+ [McpClient.CLAUDE_CODE]: {
31
+ name: 'Claude Code',
32
+ agent: AgentClient.CLAUDE_CODE,
33
+ step: 'restart Claude Code, then /mcp shows reticle',
34
+ },
35
+ [McpClient.CURSOR]: {
36
+ name: 'Cursor',
37
+ agent: AgentClient.CURSOR,
38
+ step: 'restart Cursor, then turn Reticle on in Settings › MCP',
39
+ },
40
+ [McpClient.CODEX]: {
41
+ name: 'Codex',
42
+ agent: AgentClient.CODEX,
43
+ step: 'restart Codex, then /mcp shows reticle',
44
+ },
45
+ [McpClient.GEMINI]: {
46
+ name: 'Gemini CLI',
47
+ agent: AgentClient.GEMINI_CLI,
48
+ step: 'restart Gemini CLI, then /mcp shows reticle',
49
+ },
50
+ [McpClient.WINDSURF]: {
51
+ name: 'Windsurf',
52
+ agent: AgentClient.WINDSURF,
53
+ step: 'restart Windsurf, then turn Reticle on in its MCP settings',
54
+ },
55
+ [McpClient.VSCODE]: {
56
+ name: 'VS Code',
57
+ agent: AgentClient.VSCODE,
58
+ step: 'run "Developer: Reload Window" in VS Code',
59
+ },
60
+ [McpClient.OPENCODE]: {
61
+ name: 'OpenCode',
62
+ agent: AgentClient.OPENCODE,
63
+ step: 'restart OpenCode',
64
+ },
65
+ [McpClient.ANTIGRAVITY]: {
66
+ name: 'Antigravity',
67
+ agent: AgentClient.ANTIGRAVITY,
68
+ step: 'restart Antigravity',
69
+ },
70
+ };
71
+ const NextStepCopy = {
72
+ HEADING: 'Your agents:',
73
+ PLUGIN_ACTIVE: 'plugin active: type /reload-plugins if it is open',
74
+ PLUGIN_NOT_CONNECTED: 'start Claude Code, or type /reload-plugins in an open session',
75
+ ADDED: 'added: ',
76
+ ALREADY: 'already set up',
77
+ NOT_ADDED: 'not added: ',
78
+ FAILED: 'registration failed: ',
79
+ FAILED_FILE: 'check the permissions of its config file, then run `reticle setup mcp`',
80
+ MANUAL_HEAD: 'add the reticle entry to ',
81
+ MANUAL_TAIL: ' by hand',
82
+ MISSING: 'not added: run `reticle setup mcp`',
83
+ CONNECTED_HEAD: 'Connected: ',
84
+ CONNECTED_TAIL: ' ✓',
85
+ NOT_CONNECTED: ' not connected yet: ',
86
+ DUPLICATE: ' ⚠ a user-scope reticle server is registered too, so Claude Code has two. Remove it: ',
87
+ STALE: ' ⚠ ',
88
+ };
89
+ const NAME_COLUMN = Math.max(...Object.values(AGENT_STEPS).map((s) => s.name.length)) + 2;
90
+ /**
91
+ * The rows for one `setupMcp` run, in its order.
92
+ *
93
+ * @param launcher the installed bin that run registered (`SetupMcpIo.launcher`), so a retry line
94
+ * names the same command.
95
+ */
96
+ export function agentRows(result, launcher) {
97
+ const known = (id) => id in AGENT_STEPS;
98
+ const rows = [];
99
+ for (const id of result.detected) {
100
+ if (!known(id)) {
101
+ continue;
102
+ }
103
+ if (result.pluginActive.includes(id)) {
104
+ rows.push(pluginRow(id, result.duplicates.includes(id)));
105
+ continue;
106
+ }
107
+ const manual = result.manual.find((m) => m.id === id);
108
+ if (manual !== undefined) {
109
+ rows.push({ id, reg: AgentReg.MANUAL, path: manual.configPath });
110
+ }
111
+ else if (result.failed.includes(id)) {
112
+ rows.push(McpClient.CLAUDE_CODE === id
113
+ ? { id, reg: AgentReg.FAILED, retry: claudeAddCommand(launcher).display }
114
+ : { id, reg: AgentReg.FAILED });
115
+ }
116
+ else if (result.alreadyThere.includes(id)) {
117
+ rows.push(alreadyRow(id, result.stale.includes(id), launcher));
118
+ }
119
+ else if (result.registered.includes(id)) {
120
+ rows.push({ id, reg: AgentReg.ADDED });
121
+ }
122
+ }
123
+ return rows;
124
+ }
125
+ function alreadyRow(id, stale, launcher) {
126
+ return stale
127
+ ? { id, reg: AgentReg.ALREADY, stale: claudeStaleEntryNote(launcher) }
128
+ : { id, reg: AgentReg.ALREADY };
129
+ }
130
+ function pluginRow(id, duplicate) {
131
+ return duplicate ? { id, reg: AgentReg.PLUGIN, duplicate } : { id, reg: AgentReg.PLUGIN };
132
+ }
133
+ function failedStep(row) {
134
+ return row.retry === undefined ? NextStepCopy.FAILED_FILE : `run \`${row.retry}\``;
135
+ }
136
+ /** What one row says before anything is known about the connection. */
137
+ function registrationText(row) {
138
+ const { step } = AGENT_STEPS[row.id];
139
+ switch (row.reg) {
140
+ case AgentReg.PLUGIN:
141
+ return NextStepCopy.PLUGIN_ACTIVE;
142
+ case AgentReg.ADDED:
143
+ return `${NextStepCopy.ADDED}${step}`;
144
+ case AgentReg.ALREADY:
145
+ return NextStepCopy.ALREADY;
146
+ case AgentReg.FAILED:
147
+ return `${NextStepCopy.FAILED}${failedStep(row)}`;
148
+ case AgentReg.MANUAL:
149
+ return `${NextStepCopy.NOT_ADDED}${NextStepCopy.MANUAL_HEAD}${row.path ?? ''}${NextStepCopy.MANUAL_TAIL}`;
150
+ case AgentReg.MISSING:
151
+ return NextStepCopy.MISSING;
152
+ }
153
+ }
154
+ /** The "Your agents:" block: one line per client, each with its own next step. */
155
+ export function agentLines(rows) {
156
+ if (0 === rows.length) {
157
+ return [];
158
+ }
159
+ return [
160
+ NextStepCopy.HEADING,
161
+ ...rows.flatMap((row) => {
162
+ const lines = [` ${AGENT_STEPS[row.id].name.padEnd(NAME_COLUMN)}${registrationText(row)}`];
163
+ if (true === row.duplicate) {
164
+ lines.push(`${NextStepCopy.DUPLICATE}${claudeRemoveUserCommand()}`);
165
+ }
166
+ if (row.stale !== undefined) {
167
+ lines.push(`${NextStepCopy.STALE}${row.stale}`);
168
+ }
169
+ return lines;
170
+ }),
171
+ ];
172
+ }
173
+ /** A row whose client has Reticle in its config, so a connection can be waited for. */
174
+ function isRegistered(row) {
175
+ return AgentReg.PLUGIN === row.reg || AgentReg.ADDED === row.reg || AgentReg.ALREADY === row.reg;
176
+ }
177
+ /** The clients worth waiting for: the ones that have Reticle registered. */
178
+ export function awaitedClients(rows) {
179
+ return rows.filter(isRegistered).map((row) => AGENT_STEPS[row.id].agent);
180
+ }
181
+ function missingStep(row) {
182
+ if (AgentReg.PLUGIN === row.reg) {
183
+ return NextStepCopy.PLUGIN_NOT_CONNECTED;
184
+ }
185
+ if (isRegistered(row)) {
186
+ return AGENT_STEPS[row.id].step;
187
+ }
188
+ return registrationText(row);
189
+ }
190
+ /** Per client: connected, or the exact step still missing. */
191
+ export function agentStatuses(rows, connected) {
192
+ return rows.map((row) => {
193
+ const { name, agent } = AGENT_STEPS[row.id];
194
+ if (isRegistered(row) && connected.has(agent)) {
195
+ return {
196
+ name,
197
+ connected: true,
198
+ said: `${NextStepCopy.CONNECTED_HEAD}${name}${NextStepCopy.CONNECTED_TAIL}`,
199
+ };
200
+ }
201
+ const next = missingStep(row);
202
+ const said = isRegistered(row)
203
+ ? `${name}${NextStepCopy.NOT_CONNECTED}${next}`
204
+ : `${name} ${next}`;
205
+ return { name, connected: false, next, said };
206
+ });
207
+ }
208
+ /** The lines the installer prints after its wait. */
209
+ export function connectionLines(rows, connected) {
210
+ return agentStatuses(rows, connected).map((status) => status.said);
211
+ }
212
+ /**
213
+ * Poll `read` until every client in `want` has connected, or `budgetMs` has passed. Returns the
214
+ * last read. Bounded: it never waits longer than the budget.
215
+ */
216
+ export async function waitForAgents(want, read, clock, budgetMs, pollMs) {
217
+ const start = clock.now();
218
+ let seen = read();
219
+ while (!want.every((client) => seen.has(client)) && clock.now() - start < budgetMs) {
220
+ await clock.sleep(pollMs);
221
+ seen = read();
222
+ }
223
+ return seen;
224
+ }
225
+ const CLAUDE_CONFIG_DIR_ENV = 'CLAUDE_CONFIG_DIR';
226
+ /** `$CLAUDE_CONFIG_DIR`, else home: where Claude Code keeps `.claude.json`. */
227
+ export function claudeStateDir() {
228
+ return process.env[CLAUDE_CONFIG_DIR_ENV] ?? homedir();
229
+ }
230
+ const CLAUDE_STATE_FILE = '.claude.json';
231
+ /**
232
+ * Does this client's config name a `reticle` server, in any shape?
233
+ *
234
+ * Presence, not "the entry we would write now". An entry that starts an installed launcher, or an
235
+ * older npx entry, still starts Reticle. `setup mcp` repairs a stale entry; `status` only reports
236
+ * whether one is there. A file that does not parse reads as absent.
237
+ */
238
+ function configNamesReticle(spec, existing) {
239
+ if (ConfigFormat.TOML === spec.format) {
240
+ return codexNamesOurServer(existing, spec.serversKey);
241
+ }
242
+ if (null === existing) {
243
+ return false;
244
+ }
245
+ try {
246
+ const parsed = JSON.parse(existing);
247
+ const servers = 'object' === typeof parsed && null !== parsed
248
+ ? parsed[spec.serversKey]
249
+ : undefined;
250
+ return 'object' === typeof servers && null !== servers && MCP_SERVER_NAME in servers;
251
+ }
252
+ catch {
253
+ return false;
254
+ }
255
+ }
256
+ /**
257
+ * The agents on this machine and whether Reticle is in each config, read only. For `reticle status`,
258
+ * which must not register anything and must not spawn a client CLI.
259
+ *
260
+ * @param claudeStateDir the directory that holds `.claude.json`: `$CLAUDE_CONFIG_DIR`, else home.
261
+ * @param launcher the installed bin a new registration would use, for the stale-entry fix.
262
+ */
263
+ export function installedAgentRows(io, projectDir, pluginActive, claudeStateDir = io.homeDir(), launcher) {
264
+ const rows = [];
265
+ if (pluginActive) {
266
+ // A user-scope entry next to the plugin is a second server.
267
+ rows.push(pluginRow(McpClient.CLAUDE_CODE, claudeHasReticle(io, undefined, claudeStateDir)));
268
+ }
269
+ else if (claudeHasReticle(io, projectDir, claudeStateDir)) {
270
+ rows.push(alreadyRow(McpClient.CLAUDE_CODE, claudeUserEntryIsFloating(io, claudeStateDir), launcher));
271
+ }
272
+ else if (io.exists(join(claudeStateDir, CLAUDE_STATE_FILE))) {
273
+ rows.push({ id: McpClient.CLAUDE_CODE, reg: AgentReg.MISSING });
274
+ }
275
+ for (const client of detectMcpClients(io)) {
276
+ rows.push({
277
+ id: client.id,
278
+ reg: configNamesReticle(clientSpec(client.id), client.existing)
279
+ ? AgentReg.ALREADY
280
+ : AgentReg.MISSING,
281
+ });
282
+ }
283
+ return rows;
284
+ }
@@ -13,8 +13,11 @@ import { AgentAction, AGENT_CLIENTS, joinFor, parentOf, } from './agent-configs.
13
13
  /** Two spaces, because that is what every one of these files already uses. */
14
14
  const INDENT = 2;
15
15
  const RETICLE_KEY = 'reticle';
16
- /** A fresh Continue config. YAML is only ever created, never edited. */
17
- const CONTINUE_YAML = 'mcpServers:\n - name: reticle\n type: stdio\n command: npx\n args: ["@reticlehq/server", "mcp"]\n';
16
+ /**
17
+ * A fresh Continue config. YAML is only ever created, never edited. The args are a JSON array,
18
+ * which is valid YAML flow syntax and keeps a Windows path escaped.
19
+ */
20
+ const continueYaml = (entry) => `mcpServers:\n - name: ${RETICLE_KEY}\n type: stdio\n command: ${String(entry['command'])}\n args: ${JSON.stringify(entry['args'])}\n`;
18
21
  /**
19
22
  * Write what the plan says to write, and nothing else.
20
23
  *
@@ -32,8 +35,8 @@ export function applyAgentPlan(plan, io) {
32
35
  }
33
36
  try {
34
37
  io.mkdirp(parentOf(step.file));
35
- if (AgentAction.CREATE_YAML === step.action) {
36
- io.writeFile(step.file, CONTINUE_YAML);
38
+ if (AgentAction.CREATE_YAML === step.action && undefined !== step.entry) {
39
+ io.writeFile(step.file, continueYaml(step.entry));
37
40
  return { ...base, action: 'created' };
38
41
  }
39
42
  const key = step.key;
@@ -99,10 +102,12 @@ export function applyAgentSkills(io, where) {
99
102
  const join = joinFor(where.platform);
100
103
  for (const client of AGENT_CLIENTS) {
101
104
  const dir = client.skillDir;
102
- if (undefined === dir)
105
+ if (undefined === dir) {
103
106
  continue;
104
- if (!io.exists(join(where.home, client.marker[where.platform])))
107
+ }
108
+ if (!io.exists(join(where.home, client.marker[where.platform]))) {
105
109
  continue;
110
+ }
106
111
  try {
107
112
  const target = join(where.home, dir);
108
113
  io.mkdirp(target);
@@ -1,6 +1,7 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { spanSync } from '../../../trace.js';
3
3
  import { hookBinFor } from '../../cli/hook/hook-fast.js';
4
+ import { mcpLauncher } from '../mcp-launcher.js';
4
5
  import { reportInitOutcome } from '../../../telemetry/init-telemetry.js';
5
6
  import { reportOnboardingStep } from '../../../telemetry/onboarding-funnel.js';
6
7
  import { defaultPairingTokenDir, readOrCreatePairingTokenSync, } from '../../../portal/bridge/pairing-token.js';
@@ -44,6 +45,7 @@ export function serverInitHost() {
44
45
  hookBin() {
45
46
  return hookBinFor(homedir(), process.platform);
46
47
  },
48
+ mcpLauncher,
47
49
  insideClaudeCode() {
48
50
  return CLAUDE_CODE_MARKER_VALUE === process.env[CLAUDE_CODE_MARKER_ENV];
49
51
  },
@@ -53,7 +53,12 @@ interface InitRuntimeArgs {
53
53
  interface RuntimePrintIo {
54
54
  readonly print: (line: string) => void;
55
55
  }
56
+ /**
57
+ * Print the "Your agents:" block, and return the bounded wait for the agents to connect. `mayWait`
58
+ * false: the wait is a no-op. The caller adds the terminal check. See `agentsBlock`.
59
+ */
60
+ export type AgentsBlockPort = (print: (line: string) => void, mayWait: boolean) => () => Promise<void>;
56
61
  export declare function continueAfterInit(parsed: InitRuntimeArgs, result: InitResult, io: RuntimePrintIo, cwd: string,
57
62
  /** init's ending once the connection is proved: coverage and the agent prompt. See `FirstFlowPort`. */
58
- firstFlow?: FirstFlowPort): Promise<void>;
63
+ firstFlow?: FirstFlowPort, agentsBlock?: AgentsBlockPort): Promise<void>;
59
64
  export {};
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { existsSync, readFileSync, writeFileSync } from 'node:fs';
9
9
  import { join } from 'node:path';
10
- import { InitConfirmation, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
10
+ import { InitConfirmation, OnboardingPhase, OnboardingStop, RETICLE_DEFAULT_PORT, } from '@reticlehq/core';
11
11
  import { COMMUNITY_HINT, FEEDBACK_HINT, Framework, InitFailure, } from '@reticlehq/init';
12
12
  import { confirmInstall, nodeConfirmDeps } from '../terminal/confirm.js';
13
13
  import { writeLicenseKey } from '../license-key.js';
@@ -24,11 +24,14 @@ import { collectEnv, DEFAULT_PHASE_TIMEOUT_MS } from '../setup-options.js';
24
24
  import { staticPageDevCommand } from '../bringup/static-page-server.js';
25
25
  import { SetupPhase } from '../run-setup.js';
26
26
  import { reportInitOutcome } from '../../../telemetry/init-telemetry.js';
27
+ import { reportOnboardingStop } from '../../../telemetry/onboarding-funnel.js';
27
28
  /** Claude Code marks every process it spawns with this variable set to this value. */
28
29
  const CLAUDE_CODE_ENV = 'CLAUDECODE';
29
30
  const CLAUDE_CODE_MARKER = '1';
30
31
  /** How often the runtime phases look again: fast enough not to be the wait, slow enough to be free. */
31
32
  const POLL_MS = 250;
33
+ /** The step an agent without the `reticle_*` tools cannot take. */
34
+ const FIRST_LOOK_STEP = 'first_look';
32
35
  /**
33
36
  * The shapes whose served HTML carries the SDK marker: Vite injects it into index.html, and a plain
34
37
  * HTML page has the snippet pasted in. Everything else connects from the JS bundle, where a fetch
@@ -47,8 +50,9 @@ const NEXT_APP_CONNECT = ['app', 'src/app'].flatMap((d) => ['tsx', 'jsx', 'js'].
47
50
  * names chunks, not modules, so it stays silent.
48
51
  */
49
52
  export function htmlCarriesSdk(framework, exists) {
50
- if (HTML_CARRIES_SDK.has(framework))
53
+ if (HTML_CARRIES_SDK.has(framework)) {
51
54
  return true;
55
+ }
52
56
  return Framework.NEXT === framework && NEXT_APP_CONNECT.some(exists);
53
57
  }
54
58
  /** Registration is global MCP wiring, so both flags that disown it are read here. */
@@ -68,8 +72,9 @@ const licenseIo = {
68
72
  * thing as an install working.
69
73
  */
70
74
  function printRelaunch(parsed, io, cwd) {
71
- if (true !== parsed.relaunch)
75
+ if (true !== parsed.relaunch) {
72
76
  return;
77
+ }
73
78
  io.print(relaunchDecision({
74
79
  ...(undefined === process.env['CLAUDE_CODE_SESSION_ID']
75
80
  ? {}
@@ -82,13 +87,15 @@ function printRelaunch(parsed, io, cwd) {
82
87
  }
83
88
  export async function continueAfterInit(parsed, result, io, cwd,
84
89
  /** init's ending once the connection is proved: coverage and the agent prompt. See `FirstFlowPort`. */
85
- firstFlow) {
90
+ firstFlow, agentsBlock) {
86
91
  const port = parsed.port ?? RETICLE_DEFAULT_PORT;
87
92
  const stop = () => {
88
- if (true === parsed.keepGoing)
93
+ if (true === parsed.keepGoing) {
89
94
  process.exitCode = 1;
90
- else
95
+ }
96
+ else {
91
97
  process.exit(1);
98
+ }
92
99
  };
93
100
  // Real installs persist the key whichever way setup ends. A preview must leave .env and
94
101
  // .gitignore untouched, just as it leaves project wiring and agent configuration untouched.
@@ -114,16 +121,19 @@ firstFlow) {
114
121
  registerOtherAgents(io.print);
115
122
  }
116
123
  return confirmInstall(result, io, nodeConfirmDeps(port)).then(() => {
117
- if (true === parsed.json)
124
+ if (true === parsed.json) {
118
125
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
119
- if (!result.ok)
126
+ }
127
+ if (!result.ok) {
120
128
  stop();
129
+ }
121
130
  });
122
131
  }
123
132
  // From here on init left the ask to us, so it is the last thing on every exit below.
124
133
  const ask = (spaced = true) => {
125
- if (spaced)
134
+ if (spaced) {
126
135
  io.print('');
136
+ }
127
137
  io.print(FEEDBACK_HINT);
128
138
  };
129
139
  // `init` deferred its outcome to this phase, so every exit below reports it, and only once: the
@@ -131,8 +141,9 @@ firstFlow) {
131
141
  // would double the funnel this event exists to measure.
132
142
  let outcomeReported = false;
133
143
  const reportOnce = (init) => {
134
- if (outcomeReported)
144
+ if (outcomeReported) {
135
145
  return;
146
+ }
136
147
  outcomeReported = true;
137
148
  reportInitOutcome(init);
138
149
  };
@@ -140,11 +151,13 @@ firstFlow) {
140
151
  if (context === undefined) {
141
152
  // Nothing was established, so there is nothing to run against. init has already said why, and
142
153
  // the outcome it deferred (a failed dependency install, usually) is reported here or nowhere.
143
- if (result.outcome !== undefined)
154
+ if (result.outcome !== undefined) {
144
155
  reportOnce(result.outcome);
156
+ }
145
157
  ask();
146
- if (true === parsed.json)
158
+ if (true === parsed.json) {
147
159
  process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
160
+ }
148
161
  stop();
149
162
  return;
150
163
  }
@@ -188,6 +201,9 @@ firstFlow) {
188
201
  // A Ctrl-C during the wait ends the process with no `finally`, so the outcome is reported from the
189
202
  // signal itself. The send is detached, so it survives the exit that follows.
190
203
  const releaseInterrupt = reportOnInterrupt(() => reportFailure(InitFailure.INTERRUPTED));
204
+ // The wait for the agents runs last, after the interrupt handler is gone: a Ctrl+C that skips the
205
+ // wait is not a failed init.
206
+ let waitForAgents = () => Promise.resolve();
191
207
  return runSetupCommand({
192
208
  appDir: context.appDir,
193
209
  invokedAt: cwd,
@@ -217,10 +233,12 @@ firstFlow) {
217
233
  // A plain page with no dev script gets Reticle's own static server, so the page is actually
218
234
  // served and the connect can be proved, instead of stopping at "start the app yourself".
219
235
  ...(() => {
220
- if (undefined !== context.devCommand)
236
+ if (undefined !== context.devCommand) {
221
237
  return { devCommand: context.devCommand };
222
- if (undefined !== parsed.url)
238
+ }
239
+ if (undefined !== parsed.url) {
223
240
  return {};
241
+ }
224
242
  const devCommand = staticPageDevCommand({
225
243
  appDir: context.appDir,
226
244
  isStaticPage: Framework.HTML === context.framework,
@@ -249,16 +267,18 @@ firstFlow) {
249
267
  outcome.reachedPhase === SetupPhase.DEV_SERVER
250
268
  ? InitFailure.DEV_SERVER
251
269
  : InitFailure.APP_CONNECTION;
252
- if (outcome.stop !== undefined)
270
+ if (outcome.stop !== undefined) {
253
271
  init.stop = outcome.stop;
272
+ }
254
273
  }
255
274
  reportOnce(init);
256
275
  }
257
276
  // One object, so an agent reads a result instead of interpreting a report.
258
277
  if (true === parsed.json) {
259
278
  process.stdout.write(`${JSON.stringify(outcome, null, 2)}\n`);
260
- if (!outcome.ok)
279
+ if (!outcome.ok) {
261
280
  stop();
281
+ }
262
282
  return;
263
283
  }
264
284
  io.print('');
@@ -274,7 +294,18 @@ firstFlow) {
274
294
  });
275
295
  if (undefined !== restart) {
276
296
  io.print(restart);
297
+ // The same block and wait as `reticle setup mcp`. No wait for a failed setup, or inside
298
+ // `reticle connect`, which signs in next.
299
+ if (agentsBlock !== undefined) {
300
+ waitForAgents = agentsBlock(io.print, outcome.ok && true !== parsed.keepGoing);
301
+ }
277
302
  io.print('');
303
+ // The agent cannot take its first look until it restarts. Detached: init exits soon.
304
+ reportOnboardingStop({
305
+ phase: OnboardingPhase.ONBOARD,
306
+ step: FIRST_LOOK_STEP,
307
+ stop: OnboardingStop.AGENT_NOT_RESTARTED,
308
+ }, { detach: true });
278
309
  }
279
310
  if (outcome.ok && !outcome.flowSaved) {
280
311
  // Success, and no flow. Saying "a flow was driven" here would replace a wrong exit code with
@@ -282,8 +313,9 @@ firstFlow) {
282
313
  // sentence is read by a person deciding whether their app is verified. It is not.
283
314
  // The connect already said "connected, nothing verified yet" and listed what to do next;
284
315
  // repeating it here was the same sentence twice, three lines apart.
285
- for (const [i, step] of outcome.fallback.entries())
316
+ for (const [i, step] of outcome.fallback.entries()) {
286
317
  io.print(` ${String(i + 1)}. ${step}`);
318
+ }
287
319
  // Already spaced by the blank line above when nothing was listed.
288
320
  ask(0 < outcome.fallback.length);
289
321
  io.print(COMMUNITY_HINT);
@@ -299,15 +331,17 @@ firstFlow) {
299
331
  // A run that produced no verdict did not succeed, and the exit code is the one place a caller
300
332
  // reads that without parsing anything.
301
333
  io.print('⚠ setup did not finish. To carry on from here:');
302
- for (const [i, step] of outcome.fallback.entries())
334
+ for (const [i, step] of outcome.fallback.entries()) {
303
335
  io.print(` ${i + 1}. ${step}`);
336
+ }
304
337
  ask();
305
338
  stop();
306
339
  }, (error) => {
307
340
  reportFailure(InitFailure.RUNTIME_ERROR);
308
341
  throw error;
309
342
  })
310
- .finally(releaseInterrupt);
343
+ .finally(releaseInterrupt)
344
+ .then(() => waitForAgents());
311
345
  }
312
346
  const INTERRUPT_SIGNALS = ['SIGINT', 'SIGTERM'];
313
347
  /** 128 + the signal number, which is what a shell reports. */
@@ -319,14 +353,16 @@ function reportOnInterrupt(report) {
319
353
  report();
320
354
  // Before setup installs its own handler ours may be the only one, and a listener present
321
355
  // means Node no longer exits on the signal by itself: without this, Ctrl-C would be ignored.
322
- if (1 === process.listenerCount(signal))
356
+ if (1 === process.listenerCount(signal)) {
323
357
  process.exit(INTERRUPT_EXIT_CODE[signal]);
358
+ }
324
359
  };
325
360
  process.on(signal, handler);
326
361
  return { signal, handler };
327
362
  });
328
363
  return () => {
329
- for (const { signal, handler } of handlers)
364
+ for (const { signal, handler } of handlers) {
330
365
  process.off(signal, handler);
366
+ }
331
367
  };
332
368
  }
@@ -0,0 +1,3 @@
1
+ /** `bin/reticle.js`, from `dist/command/setup/mcp-launcher.js`. The real path, not a symlink. */
2
+ export declare const CLI_BIN: string;
3
+ export declare function mcpLauncher(): string | undefined;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The bin that agent configs start the MCP server from, when this CLI is an install that stays on
3
+ * disk. Undefined for npx's cache and other run-once copies: see `installedLauncher` in init.
4
+ */
5
+ import { fileURLToPath } from 'node:url';
6
+ import { installedLauncher } from '@reticlehq/init';
7
+ /** `bin/reticle.js`, from `dist/command/setup/mcp-launcher.js`. The real path, not a symlink. */
8
+ export const CLI_BIN = fileURLToPath(new URL('../../../bin/reticle.js', import.meta.url));
9
+ export function mcpLauncher() {
10
+ return installedLauncher(CLI_BIN);
11
+ }