@a5c-ai/babysitter-cursor 0.1.1-staging.d0170171

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.
@@ -0,0 +1,369 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+ const { spawnSync } = require('child_process');
7
+
8
+ const PLUGIN_NAME = 'babysitter-cursor';
9
+ const PLUGIN_CATEGORY = 'Coding';
10
+ const HOOK_SCRIPT_NAMES = [
11
+ 'session-start.sh',
12
+ 'session-start.ps1',
13
+ 'stop-hook.sh',
14
+ 'stop-hook.ps1',
15
+ ];
16
+ const DEFAULT_MARKETPLACE = {
17
+ name: 'local-plugins',
18
+ interface: {
19
+ displayName: 'Local Plugins',
20
+ },
21
+ plugins: [],
22
+ };
23
+ const PLUGIN_BUNDLE_ENTRIES = [
24
+ '.cursor-plugin',
25
+ 'plugin.json',
26
+ 'hooks.json',
27
+ 'hooks',
28
+ 'skills',
29
+ 'versions.json',
30
+ '.cursorrules',
31
+ ];
32
+
33
+ function getCursorHome() {
34
+ if (process.env.CURSOR_HOME) return path.resolve(process.env.CURSOR_HOME);
35
+ return path.join(os.homedir(), '.cursor');
36
+ }
37
+
38
+ function getUserHome() {
39
+ if (process.env.USERPROFILE) return path.resolve(process.env.USERPROFILE);
40
+ if (process.env.HOME) return path.resolve(process.env.HOME);
41
+ return os.homedir();
42
+ }
43
+
44
+ function getGlobalStateDir() {
45
+ if (process.env.BABYSITTER_GLOBAL_STATE_DIR) {
46
+ return path.resolve(process.env.BABYSITTER_GLOBAL_STATE_DIR);
47
+ }
48
+ return path.join(getUserHome(), '.a5c');
49
+ }
50
+
51
+ function getHomePluginRoot() {
52
+ if (process.env.BABYSITTER_CURSOR_PLUGIN_DIR) {
53
+ return path.resolve(process.env.BABYSITTER_CURSOR_PLUGIN_DIR, PLUGIN_NAME);
54
+ }
55
+ return path.join(getCursorHome(), 'plugins', PLUGIN_NAME);
56
+ }
57
+
58
+ function getHomeMarketplacePath() {
59
+ if (process.env.BABYSITTER_CURSOR_MARKETPLACE_PATH) {
60
+ return path.resolve(process.env.BABYSITTER_CURSOR_MARKETPLACE_PATH);
61
+ }
62
+ return path.join(getUserHome(), '.agents', 'plugins', 'marketplace.json');
63
+ }
64
+
65
+ function writeFileIfChanged(filePath, contents) {
66
+ if (fs.existsSync(filePath)) {
67
+ const current = fs.readFileSync(filePath, 'utf8');
68
+ if (current === contents) {
69
+ return false;
70
+ }
71
+ }
72
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
73
+ fs.writeFileSync(filePath, contents, 'utf8');
74
+ return true;
75
+ }
76
+
77
+ function copyRecursive(src, dest) {
78
+ const stat = fs.statSync(src);
79
+ if (stat.isDirectory()) {
80
+ fs.mkdirSync(dest, { recursive: true });
81
+ for (const entry of fs.readdirSync(src)) {
82
+ if (['node_modules', '.git', 'test', '.a5c'].includes(entry)) continue;
83
+ copyRecursive(path.join(src, entry), path.join(dest, entry));
84
+ }
85
+ return;
86
+ }
87
+
88
+ if (path.basename(src) === 'SKILL.md') {
89
+ const file = fs.readFileSync(src);
90
+ const hasBom = file.length >= 3 && file[0] === 0xef && file[1] === 0xbb && file[2] === 0xbf;
91
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
92
+ fs.writeFileSync(dest, hasBom ? file.subarray(3) : file);
93
+ return;
94
+ }
95
+
96
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
97
+ fs.copyFileSync(src, dest);
98
+ }
99
+
100
+ function copyPluginBundle(packageRoot, pluginRoot) {
101
+ if (path.resolve(packageRoot) === path.resolve(pluginRoot)) {
102
+ return;
103
+ }
104
+ fs.rmSync(pluginRoot, { recursive: true, force: true });
105
+ fs.mkdirSync(pluginRoot, { recursive: true });
106
+ for (const entry of PLUGIN_BUNDLE_ENTRIES) {
107
+ const src = path.join(packageRoot, entry);
108
+ if (fs.existsSync(src)) {
109
+ copyRecursive(src, path.join(pluginRoot, entry));
110
+ }
111
+ }
112
+ }
113
+
114
+ function ensureExecutable(filePath) {
115
+ try {
116
+ fs.chmodSync(filePath, 0o755);
117
+ } catch {
118
+ // Best-effort only. Windows and some filesystems may ignore mode changes.
119
+ }
120
+ }
121
+
122
+ function normalizeMarketplaceSourcePath(marketplacePath, pluginSourcePath) {
123
+ let next = pluginSourcePath;
124
+ if (path.isAbsolute(next)) {
125
+ next = path.relative(path.dirname(marketplacePath), next);
126
+ }
127
+ next = String(next || '').replace(/\\/g, '/');
128
+ if (!next.startsWith('./') && !next.startsWith('../')) {
129
+ next = `./${next}`;
130
+ }
131
+ return next;
132
+ }
133
+
134
+ function readJson(filePath) {
135
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
136
+ }
137
+
138
+ function writeJson(filePath, value) {
139
+ writeFileIfChanged(filePath, `${JSON.stringify(value, null, 2)}\n`);
140
+ }
141
+
142
+ function ensureMarketplaceEntry(marketplacePath, pluginSourcePath) {
143
+ const marketplace = fs.existsSync(marketplacePath)
144
+ ? readJson(marketplacePath)
145
+ : { ...DEFAULT_MARKETPLACE, plugins: [] };
146
+ marketplace.name = marketplace.name || DEFAULT_MARKETPLACE.name;
147
+ marketplace.interface = marketplace.interface || {};
148
+ marketplace.interface.displayName =
149
+ marketplace.interface.displayName || DEFAULT_MARKETPLACE.interface.displayName;
150
+ const nextEntry = {
151
+ name: PLUGIN_NAME,
152
+ source: {
153
+ source: 'local',
154
+ path: normalizeMarketplaceSourcePath(marketplacePath, pluginSourcePath),
155
+ },
156
+ policy: {
157
+ installation: 'AVAILABLE',
158
+ authentication: 'ON_INSTALL',
159
+ },
160
+ category: PLUGIN_CATEGORY,
161
+ };
162
+ const existingIndex = Array.isArray(marketplace.plugins)
163
+ ? marketplace.plugins.findIndex((entry) => entry && entry.name === PLUGIN_NAME)
164
+ : -1;
165
+ if (!Array.isArray(marketplace.plugins)) {
166
+ marketplace.plugins = [nextEntry];
167
+ } else if (existingIndex >= 0) {
168
+ marketplace.plugins[existingIndex] = nextEntry;
169
+ } else {
170
+ marketplace.plugins.push(nextEntry);
171
+ }
172
+ writeJson(marketplacePath, marketplace);
173
+ return nextEntry;
174
+ }
175
+
176
+ function removeMarketplaceEntry(marketplacePath) {
177
+ if (!fs.existsSync(marketplacePath)) {
178
+ return;
179
+ }
180
+ const marketplace = readJson(marketplacePath);
181
+ if (!Array.isArray(marketplace.plugins)) {
182
+ return;
183
+ }
184
+ marketplace.plugins = marketplace.plugins.filter((entry) => entry && entry.name !== PLUGIN_NAME);
185
+ writeJson(marketplacePath, marketplace);
186
+ }
187
+
188
+ function mergeManagedHooksConfig(packageRoot, cursorHome) {
189
+ const hooksJsonPath = path.join(packageRoot, 'hooks.json');
190
+ if (!fs.existsSync(hooksJsonPath)) return;
191
+ const managedConfig = readJson(hooksJsonPath);
192
+ const managedHooks = managedConfig.hooks || {};
193
+ const hooksConfigPath = path.join(cursorHome, 'hooks.json');
194
+ const existing = fs.existsSync(hooksConfigPath)
195
+ ? readJson(hooksConfigPath)
196
+ : { version: 1, hooks: {} };
197
+ existing.version = existing.version || 1;
198
+ if (!existing.hooks || typeof existing.hooks !== 'object') {
199
+ existing.hooks = {};
200
+ }
201
+
202
+ for (const [eventName, entries] of Object.entries(managedHooks)) {
203
+ const existingEntries = Array.isArray(existing.hooks[eventName]) ? existing.hooks[eventName] : [];
204
+ const filteredEntries = existingEntries
205
+ .filter((entry) => {
206
+ const bash = String(entry.bash || entry.command || '');
207
+ const ps = String(entry.powershell || '');
208
+ return !HOOK_SCRIPT_NAMES.some((name) => bash.includes(name) || ps.includes(name));
209
+ });
210
+ existing.hooks[eventName] = [...filteredEntries, ...entries];
211
+ }
212
+
213
+ writeJson(hooksConfigPath, existing);
214
+ }
215
+
216
+ function removeManagedHooks(cursorHome) {
217
+ for (const hookName of HOOK_SCRIPT_NAMES) {
218
+ fs.rmSync(path.join(cursorHome, 'hooks', hookName), { force: true });
219
+ }
220
+
221
+ const hooksConfigPath = path.join(cursorHome, 'hooks.json');
222
+ if (!fs.existsSync(hooksConfigPath)) {
223
+ return;
224
+ }
225
+ let hooksConfig;
226
+ try {
227
+ hooksConfig = readJson(hooksConfigPath);
228
+ } catch {
229
+ return;
230
+ }
231
+ if (!hooksConfig.hooks || typeof hooksConfig.hooks !== 'object') {
232
+ return;
233
+ }
234
+ for (const eventName of Object.keys(hooksConfig.hooks)) {
235
+ const eventHooks = Array.isArray(hooksConfig.hooks[eventName]) ? hooksConfig.hooks[eventName] : [];
236
+ const filtered = eventHooks.filter((entry) => {
237
+ const bash = String(entry.bash || entry.command || '');
238
+ const ps = String(entry.powershell || '');
239
+ return !HOOK_SCRIPT_NAMES.some((name) => bash.includes(name) || ps.includes(name));
240
+ });
241
+ if (filtered.length > 0) {
242
+ hooksConfig.hooks[eventName] = filtered;
243
+ } else {
244
+ delete hooksConfig.hooks[eventName];
245
+ }
246
+ }
247
+ if (Object.keys(hooksConfig.hooks).length === 0) {
248
+ fs.rmSync(hooksConfigPath, { force: true });
249
+ } else {
250
+ writeJson(hooksConfigPath, hooksConfig);
251
+ }
252
+ }
253
+
254
+ function installCursorSurface(packageRoot, cursorHome) {
255
+ // Install skills
256
+ const sourceSkills = path.join(packageRoot, 'skills');
257
+ if (fs.existsSync(sourceSkills)) {
258
+ const targetSkills = path.join(cursorHome, 'skills');
259
+ fs.mkdirSync(targetSkills, { recursive: true });
260
+ for (const entry of fs.readdirSync(sourceSkills, { withFileTypes: true })) {
261
+ if (!entry.isDirectory()) continue;
262
+ copyRecursive(
263
+ path.join(sourceSkills, entry.name),
264
+ path.join(targetSkills, entry.name),
265
+ );
266
+ }
267
+ }
268
+
269
+ // Install hooks
270
+ const sourceHooks = path.join(packageRoot, 'hooks');
271
+ if (fs.existsSync(sourceHooks)) {
272
+ const targetHooks = path.join(cursorHome, 'hooks');
273
+ fs.mkdirSync(targetHooks, { recursive: true });
274
+ for (const scriptName of HOOK_SCRIPT_NAMES) {
275
+ const sourcePath = path.join(sourceHooks, scriptName);
276
+ if (!fs.existsSync(sourcePath)) continue;
277
+ const targetPath = path.join(targetHooks, scriptName);
278
+ copyRecursive(sourcePath, targetPath);
279
+ ensureExecutable(targetPath);
280
+ }
281
+ }
282
+
283
+ // Merge hooks.json config
284
+ mergeManagedHooksConfig(packageRoot, cursorHome);
285
+
286
+ // Install .cursorrules
287
+ const sourceRules = path.join(packageRoot, '.cursorrules');
288
+ if (fs.existsSync(sourceRules)) {
289
+ copyRecursive(sourceRules, path.join(cursorHome, '.cursorrules'));
290
+ }
291
+ }
292
+
293
+ function resolveBabysitterCommand(packageRoot) {
294
+ if (process.env.BABYSITTER_SDK_CLI) {
295
+ return {
296
+ command: process.execPath,
297
+ argsPrefix: [path.resolve(process.env.BABYSITTER_SDK_CLI)],
298
+ };
299
+ }
300
+ try {
301
+ return {
302
+ command: process.execPath,
303
+ argsPrefix: [
304
+ require.resolve('@a5c-ai/babysitter-sdk/dist/cli/main.js', {
305
+ paths: [packageRoot],
306
+ }),
307
+ ],
308
+ };
309
+ } catch {
310
+ return {
311
+ command: 'babysitter',
312
+ argsPrefix: [],
313
+ };
314
+ }
315
+ }
316
+
317
+ function runBabysitterCli(packageRoot, cliArgs, options = {}) {
318
+ const resolved = resolveBabysitterCommand(packageRoot);
319
+ const result = spawnSync(resolved.command, [...resolved.argsPrefix, ...cliArgs], {
320
+ cwd: options.cwd || process.cwd(),
321
+ stdio: ['ignore', 'pipe', 'pipe'],
322
+ encoding: 'utf8',
323
+ env: {
324
+ ...process.env,
325
+ ...(options.env || {}),
326
+ },
327
+ });
328
+ if (result.status !== 0) {
329
+ const stderr = (result.stderr || '').trim();
330
+ const stdout = (result.stdout || '').trim();
331
+ throw new Error(
332
+ `babysitter ${cliArgs.join(' ')} failed` +
333
+ (stderr ? `: ${stderr}` : stdout ? `: ${stdout}` : ''),
334
+ );
335
+ }
336
+ return result.stdout;
337
+ }
338
+
339
+ function ensureGlobalProcessLibrary(packageRoot) {
340
+ return JSON.parse(
341
+ runBabysitterCli(
342
+ packageRoot,
343
+ ['process-library:active', '--state-dir', getGlobalStateDir(), '--json'],
344
+ { cwd: packageRoot },
345
+ ),
346
+ );
347
+ }
348
+
349
+ function warnWindowsHooks() {
350
+ if (process.platform !== 'win32') {
351
+ return;
352
+ }
353
+ console.warn(`[${PLUGIN_NAME}] Note: On Windows, Cursor will use .ps1 PowerShell hooks.`);
354
+ console.warn(`[${PLUGIN_NAME}] Both bash (.sh) and PowerShell (.ps1) hook scripts are included.`);
355
+ }
356
+
357
+ module.exports = {
358
+ copyPluginBundle,
359
+ ensureGlobalProcessLibrary,
360
+ ensureMarketplaceEntry,
361
+ getCursorHome,
362
+ getHomeMarketplacePath,
363
+ getHomePluginRoot,
364
+ installCursorSurface,
365
+ removeManagedHooks,
366
+ removeMarketplaceEntry,
367
+ warnWindowsHooks,
368
+ writeJson,
369
+ };
package/bin/install.js ADDED
@@ -0,0 +1,46 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const {
6
+ copyPluginBundle,
7
+ ensureGlobalProcessLibrary,
8
+ ensureMarketplaceEntry,
9
+ getCursorHome,
10
+ getHomeMarketplacePath,
11
+ getHomePluginRoot,
12
+ installCursorSurface,
13
+ warnWindowsHooks,
14
+ } = require('./install-shared');
15
+
16
+ const PACKAGE_ROOT = path.resolve(__dirname, '..');
17
+
18
+ function main() {
19
+ const cursorHome = getCursorHome();
20
+ const pluginRoot = getHomePluginRoot();
21
+ const marketplacePath = getHomeMarketplacePath();
22
+
23
+ console.log(`[babysitter-cursor] Installing plugin to ${pluginRoot}`);
24
+
25
+ try {
26
+ copyPluginBundle(PACKAGE_ROOT, pluginRoot);
27
+ ensureMarketplaceEntry(marketplacePath, pluginRoot);
28
+ installCursorSurface(PACKAGE_ROOT, cursorHome);
29
+
30
+ const active = ensureGlobalProcessLibrary(PACKAGE_ROOT);
31
+ console.log(`[babysitter-cursor] marketplace: ${marketplacePath}`);
32
+ console.log(`[babysitter-cursor] process library: ${active.binding?.dir}`);
33
+ if (active.defaultSpec?.cloneDir) {
34
+ console.log(`[babysitter-cursor] process library clone: ${active.defaultSpec.cloneDir}`);
35
+ }
36
+ console.log(`[babysitter-cursor] process library state: ${active.stateFile}`);
37
+ warnWindowsHooks();
38
+ console.log('[babysitter-cursor] Installation complete!');
39
+ console.log('[babysitter-cursor] Restart Cursor to pick up the installed plugin and config changes.');
40
+ } catch (err) {
41
+ console.error(`[babysitter-cursor] Failed to install plugin: ${err.message}`);
42
+ process.exitCode = 1;
43
+ }
44
+ }
45
+
46
+ main();
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const {
6
+ getCursorHome,
7
+ getHomeMarketplacePath,
8
+ getHomePluginRoot,
9
+ removeManagedHooks,
10
+ removeMarketplaceEntry,
11
+ } = require('./install-shared');
12
+
13
+ function main() {
14
+ const cursorHome = getCursorHome();
15
+ const pluginRoot = getHomePluginRoot();
16
+ const marketplacePath = getHomeMarketplacePath();
17
+ let removedPlugin = false;
18
+
19
+ if (fs.existsSync(pluginRoot)) {
20
+ try {
21
+ fs.rmSync(pluginRoot, { recursive: true, force: true });
22
+ console.log(`[babysitter-cursor] Removed ${pluginRoot}`);
23
+ removedPlugin = true;
24
+ } catch (err) {
25
+ console.warn(`[babysitter-cursor] Warning: Could not remove plugin directory ${pluginRoot}: ${err.message}`);
26
+ }
27
+ }
28
+
29
+ removeMarketplaceEntry(marketplacePath);
30
+ removeManagedHooks(cursorHome);
31
+
32
+ if (!removedPlugin) {
33
+ console.log('[babysitter-cursor] Plugin directory not found, config and hooks cleaned if present.');
34
+ return;
35
+ }
36
+
37
+ console.log('[babysitter-cursor] Restart Cursor to complete uninstallation.');
38
+ }
39
+
40
+ main();
@@ -0,0 +1,115 @@
1
+ # Babysitter Session Start Hook for Cursor IDE/CLI (PowerShell)
2
+ # Ensures the babysitter SDK CLI is installed (from versions.json sdkVersion),
3
+ # then delegates to the SDK hook handler.
4
+ #
5
+ # Protocol:
6
+ # Input: JSON via stdin (contains session_id, cwd, etc.)
7
+ # Output: JSON via stdout ({} on success)
8
+ # Stderr: debug/log output only
9
+ # Exit 0: success
10
+ # Exit 2: block (fatal error)
11
+
12
+ $ErrorActionPreference = "Stop"
13
+
14
+ $PluginRoot = if ($env:CURSOR_PLUGIN_ROOT) { $env:CURSOR_PLUGIN_ROOT } else { Split-Path -Parent $PSScriptRoot }
15
+ $StateDir = if ($env:BABYSITTER_STATE_DIR) { $env:BABYSITTER_STATE_DIR } else { Join-Path $PWD ".a5c" }
16
+ $MarkerFile = Join-Path $PluginRoot ".babysitter-install-attempted"
17
+
18
+ $env:CURSOR_PLUGIN_ROOT = $PluginRoot
19
+ $env:BABYSITTER_STATE_DIR = $StateDir
20
+
21
+ $LogDir = if ($env:BABYSITTER_LOG_DIR) { $env:BABYSITTER_LOG_DIR } else { Join-Path $PluginRoot ".a5c\logs" }
22
+ $LogFile = Join-Path $LogDir "babysitter-session-start-hook.log"
23
+ New-Item -ItemType Directory -Path $LogDir -Force -ErrorAction SilentlyContinue | Out-Null
24
+
25
+ function Write-Blog {
26
+ param([string]$Message)
27
+ $ts = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
28
+ Add-Content -Path $LogFile -Value "[INFO] $ts $Message" -ErrorAction SilentlyContinue
29
+ if (Get-Command babysitter -ErrorAction SilentlyContinue) {
30
+ & babysitter log --type hook --label "hook:session-start" --message $Message --source shell-hook 2>$null
31
+ }
32
+ }
33
+
34
+ Write-Blog "Hook script invoked"
35
+ Write-Blog "PLUGIN_ROOT=$PluginRoot"
36
+ Write-Blog "STATE_DIR=$StateDir"
37
+
38
+ # Get required SDK version from versions.json
39
+ $versionsFile = Join-Path $PluginRoot "versions.json"
40
+ try {
41
+ $SdkVersion = (Get-Content $versionsFile -Raw | ConvertFrom-Json).sdkVersion
42
+ if (-not $SdkVersion) { $SdkVersion = "latest" }
43
+ } catch {
44
+ $SdkVersion = "latest"
45
+ }
46
+
47
+ function Install-Sdk {
48
+ param([string]$TargetVersion)
49
+ try {
50
+ & npm i -g "@a5c-ai/babysitter-sdk@$TargetVersion" --loglevel=error 2>$null
51
+ if ($LASTEXITCODE -eq 0) {
52
+ Write-Blog "Installed SDK globally ($TargetVersion)"
53
+ return $true
54
+ }
55
+ } catch {}
56
+ try {
57
+ $prefix = Join-Path $env:USERPROFILE ".local"
58
+ & npm i -g "@a5c-ai/babysitter-sdk@$TargetVersion" --prefix $prefix --loglevel=error 2>$null
59
+ if ($LASTEXITCODE -eq 0) {
60
+ $env:PATH = "$prefix\bin;$env:PATH"
61
+ Write-Blog "Installed SDK to user prefix ($TargetVersion)"
62
+ return $true
63
+ }
64
+ } catch {}
65
+ return $false
66
+ }
67
+
68
+ # Check if babysitter CLI exists and if version matches
69
+ $NeedsInstall = $false
70
+ if (Get-Command babysitter -ErrorAction SilentlyContinue) {
71
+ $CurrentVersion = & babysitter --version 2>$null
72
+ if ($CurrentVersion -ne $SdkVersion) {
73
+ Write-Blog "SDK version mismatch: installed=$CurrentVersion, required=$SdkVersion"
74
+ $NeedsInstall = $true
75
+ } else {
76
+ Write-Blog "SDK version OK: $CurrentVersion"
77
+ }
78
+ } else {
79
+ Write-Blog "SDK CLI not found, will install"
80
+ $NeedsInstall = $true
81
+ }
82
+
83
+ # Install/upgrade if needed (only attempt once per plugin version)
84
+ if ($NeedsInstall -and -not (Test-Path $MarkerFile)) {
85
+ Install-Sdk $SdkVersion | Out-Null
86
+ Set-Content -Path $MarkerFile -Value $SdkVersion -ErrorAction SilentlyContinue
87
+ }
88
+
89
+ # If still not available after install attempt, try npx as last resort
90
+ $useFallback = $false
91
+ if (-not (Get-Command babysitter -ErrorAction SilentlyContinue)) {
92
+ Write-Blog "CLI not found after install, using npx fallback"
93
+ $useFallback = $true
94
+ }
95
+
96
+ # Capture stdin
97
+ $InputFile = [System.IO.Path]::GetTempFileName()
98
+ $input | Out-File -FilePath $InputFile -Encoding utf8
99
+
100
+ Write-Blog "Hook input received"
101
+
102
+ $stderrLog = Join-Path $LogDir "babysitter-session-start-hook-stderr.log"
103
+
104
+ if ($useFallback) {
105
+ $Result = Get-Content $InputFile | & npx -y "@a5c-ai/babysitter-sdk@$SdkVersion" hook:run --hook-type session-start --harness cursor --plugin-root $PluginRoot --state-dir $StateDir --json 2>$stderrLog
106
+ } else {
107
+ $Result = Get-Content $InputFile | & babysitter hook:run --hook-type session-start --harness cursor --plugin-root $PluginRoot --state-dir $StateDir --json 2>$stderrLog
108
+ }
109
+ $ExitCode = $LASTEXITCODE
110
+
111
+ Write-Blog "CLI exit code=$ExitCode"
112
+
113
+ Remove-Item $InputFile -Force -ErrorAction SilentlyContinue
114
+ Write-Output $Result
115
+ exit $ExitCode
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env bash
2
+ # Babysitter Session Start Hook for Cursor IDE/CLI
3
+ # Ensures the babysitter SDK CLI is installed (from versions.json sdkVersion),
4
+ # then delegates to the SDK hook handler.
5
+ #
6
+ # Protocol:
7
+ # Input: JSON via stdin (contains session_id, cwd, etc.)
8
+ # Output: JSON via stdout ({} on success)
9
+ # Stderr: debug/log output only
10
+ # Exit 0: success
11
+ # Exit 2: block (fatal error)
12
+
13
+ set -euo pipefail
14
+
15
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
16
+ PLUGIN_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
17
+ STATE_DIR="${BABYSITTER_STATE_DIR:-${PWD}/.a5c}"
18
+ LOG_DIR="${BABYSITTER_LOG_DIR:-$PLUGIN_ROOT/.a5c/logs}"
19
+ LOG_FILE="$LOG_DIR/babysitter-session-start-hook.log"
20
+
21
+ export CURSOR_PLUGIN_ROOT="${CURSOR_PLUGIN_ROOT:-${PLUGIN_ROOT}}"
22
+ export BABYSITTER_STATE_DIR="${STATE_DIR}"
23
+
24
+ mkdir -p "$LOG_DIR" 2>/dev/null
25
+
26
+ blog() {
27
+ local msg="$1"
28
+ local ts
29
+ ts="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
30
+ echo "[INFO] $ts $msg" >> "$LOG_FILE" 2>/dev/null
31
+ babysitter log --type hook --label "hook:session-start" --message "$msg" --source shell-hook 2>/dev/null || true
32
+ }
33
+
34
+ blog "Hook script invoked"
35
+ blog "PLUGIN_ROOT=$PLUGIN_ROOT"
36
+ blog "STATE_DIR=$STATE_DIR"
37
+
38
+ # Get required SDK version from versions.json
39
+ SDK_VERSION=$(node -e "try{console.log(JSON.parse(require('fs').readFileSync('${PLUGIN_ROOT}/versions.json','utf8')).sdkVersion||'latest')}catch{console.log('latest')}" 2>/dev/null || echo "latest")
40
+
41
+ # Function to install/upgrade SDK
42
+ install_sdk() {
43
+ local target_version="$1"
44
+ if npm i -g "@a5c-ai/babysitter-sdk@${target_version}" --loglevel=error 2>/dev/null; then
45
+ blog "Installed SDK globally (${target_version})"
46
+ return 0
47
+ else
48
+ if npm i -g "@a5c-ai/babysitter-sdk@${target_version}" --prefix "$HOME/.local" --loglevel=error 2>/dev/null; then
49
+ export PATH="$HOME/.local/bin:$PATH"
50
+ blog "Installed SDK to user prefix (${target_version})"
51
+ return 0
52
+ fi
53
+ fi
54
+ return 1
55
+ }
56
+
57
+ # Check if babysitter CLI exists and if version matches
58
+ NEEDS_INSTALL=false
59
+ if command -v babysitter &>/dev/null; then
60
+ CURRENT_VERSION=$(babysitter --version 2>/dev/null || echo "unknown")
61
+ if [ "$CURRENT_VERSION" != "$SDK_VERSION" ]; then
62
+ blog "SDK version mismatch: installed=${CURRENT_VERSION}, required=${SDK_VERSION}"
63
+ NEEDS_INSTALL=true
64
+ else
65
+ blog "SDK version OK: ${CURRENT_VERSION}"
66
+ fi
67
+ else
68
+ blog "SDK CLI not found, will install"
69
+ NEEDS_INSTALL=true
70
+ fi
71
+
72
+ MARKER_FILE="${PLUGIN_ROOT}/.babysitter-install-attempted"
73
+
74
+ # Install/upgrade if needed (only attempt once per plugin version)
75
+ if [ "$NEEDS_INSTALL" = true ] && [ ! -f "$MARKER_FILE" ]; then
76
+ install_sdk "$SDK_VERSION"
77
+ echo "$SDK_VERSION" > "$MARKER_FILE" 2>/dev/null
78
+ fi
79
+
80
+ # If still not available after install attempt, try npx as last resort
81
+ if ! command -v babysitter &>/dev/null; then
82
+ blog "CLI not found after install, using npx fallback"
83
+ babysitter() { npx -y "@a5c-ai/babysitter-sdk@${SDK_VERSION}" "$@"; }
84
+ export -f babysitter
85
+ fi
86
+
87
+ # Capture stdin to a temp file so the CLI receives a clean EOF
88
+ INPUT_FILE=$(mktemp 2>/dev/null || echo "/tmp/cursor-session-start-hook-$$.json")
89
+ cat > "$INPUT_FILE"
90
+
91
+ blog "Hook input received ($(wc -c < "$INPUT_FILE") bytes)"
92
+
93
+ RESULT=$(babysitter hook:run \
94
+ --hook-type session-start \
95
+ --harness cursor \
96
+ --plugin-root "$PLUGIN_ROOT" \
97
+ --state-dir "${BABYSITTER_STATE_DIR}" \
98
+ --json < "$INPUT_FILE" 2>"$LOG_DIR/babysitter-session-start-hook-stderr.log")
99
+ EXIT_CODE=$?
100
+
101
+ blog "CLI exit code=$EXIT_CODE"
102
+
103
+ rm -f "$INPUT_FILE" 2>/dev/null
104
+ printf '%s\n' "$RESULT"
105
+ exit $EXIT_CODE