@scrylog/cli 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -452,9 +452,14 @@ Setup:
452
452
  bun install -g @scrylog/opencode-plugin
453
453
  ```
454
454
 
455
- 2. Set `opencode.mode` to `hooks`.
455
+ 2. Set `opencode.mode` to `hooks` and register the plugin.
456
456
 
457
- 3. Add the plugin to `~/.config/opencode/opencode.json`:
457
+ ```bash
458
+ scrylog setup
459
+ scrylog opencode hooks install
460
+ ```
461
+
462
+ Alternatively, add the plugin to `~/.config/opencode/opencode.json` manually:
458
463
 
459
464
  ```json
460
465
  {
@@ -462,7 +467,7 @@ bun install -g @scrylog/opencode-plugin
462
467
  }
463
468
  ```
464
469
 
465
- 4. Start `scrylogd` or install the service.
470
+ 3. Start `scrylogd` or install the service.
466
471
 
467
472
  Recommended command flow:
468
473
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scrylog/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Terminal dashboard for monitoring tmux sessions running AI coding agents",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -54,7 +54,9 @@
54
54
  "start": "bun run src/tui/index.ts",
55
55
  "check-types": "tsc --noEmit",
56
56
  "test": "bun test",
57
- "pack:dry-run": "npm pack --dry-run"
57
+ "prepublishOnly": "bun run ../../scripts/resolve-catalogs.ts",
58
+ "postpublish": "bun run ../../scripts/restore-catalogs.ts",
59
+ "pack:dry-run": "bun run ../../scripts/resolve-catalogs.ts && npm pack --dry-run && bun run ../../scripts/restore-catalogs.ts"
58
60
  },
59
61
  "dependencies": {
60
62
  "@hono/zod-openapi": "^1.3.0",
@@ -64,12 +66,12 @@
64
66
  "@scalar/openapi-to-markdown": "^0.5.9",
65
67
  "hono": "^4.12.15",
66
68
  "neo-blessed": "^0.2.0",
67
- "zod": "catalog:"
69
+ "zod": "^4.3.6"
68
70
  },
69
71
  "devDependencies": {
70
72
  "@types/blessed": "^0.1.27",
71
- "@types/bun": "catalog:",
72
- "@types/node": "catalog:",
73
- "typescript": "catalog:"
73
+ "@types/bun": "^1.3.14",
74
+ "@types/node": "^24.3.0",
75
+ "typescript": "5.9.3"
74
76
  }
75
77
  }
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, copyFileSync } from "node:fs";
2
2
  import { randomBytes } from "node:crypto";
3
3
  import { readFile } from "node:fs/promises";
4
4
  import { homedir } from "node:os";
@@ -187,7 +187,20 @@ export async function runDoctor(options: GlobalCliOptions): Promise<void> {
187
187
 
188
188
  if (config.opencode.mode === "hooks") {
189
189
  process.stdout.write("OpenCode mode: hooks\n");
190
- process.stdout.write(`OpenCode plugin config present: ${existsSync(join(homedir(), ".config", "opencode", "opencode.json"))}\n`);
190
+ const opencodeSettingsPath = join(homedir(), ".config", "opencode", "opencode.json");
191
+ process.stdout.write(`OpenCode plugin config present: ${existsSync(opencodeSettingsPath)}\n`);
192
+ let pluginRegistered = false;
193
+ if (existsSync(opencodeSettingsPath)) {
194
+ try {
195
+ const opencodeSettings = JSON.parse(readFileSync(opencodeSettingsPath, "utf8")) as OpenCodeSettings;
196
+ pluginRegistered = (opencodeSettings.plugin ?? []).some(
197
+ (plugin) => typeof plugin === "string" && plugin.includes(OPENCODE_PLUGIN_NAME),
198
+ );
199
+ } catch {
200
+ // ignore parse errors; pluginRegistered stays false
201
+ }
202
+ }
203
+ process.stdout.write(`OpenCode plugin registered: ${pluginRegistered}\n`);
191
204
  } else {
192
205
  process.stdout.write(`OpenCode mode: sse\nOpenCode auto-discover: ${config.opencode.autoDiscover}\nConfigured instances: ${config.opencode.instances.length}\n`);
193
206
  }
@@ -227,6 +240,10 @@ export async function runSetupWizard(options: GlobalCliOptions): Promise<void> {
227
240
  .filter(Boolean);
228
241
  } else {
229
242
  process.stdout.write("Hooks mode requires the @scrylog/opencode-plugin plugin in your OpenCode config.\n");
243
+ const installPlugin = await prompt.yesNo("Run scrylog opencode hooks install now?", false);
244
+ if (installPlugin) {
245
+ await runOpenCodeHooksInstall(options);
246
+ }
230
247
  }
231
248
  }
232
249
 
@@ -307,6 +324,19 @@ function parseOptionalInt(value: string, fallback: number): number {
307
324
  return Number.isNaN(parsed) ? fallback : parsed;
308
325
  }
309
326
 
327
+ const OPENCODE_PLUGIN_NAME = "@scrylog/opencode-plugin";
328
+
329
+ function backupBeforeWrite(path: string): void {
330
+ if (!existsSync(path)) return;
331
+ const bak = `${path}.bak`;
332
+ copyFileSync(path, bak);
333
+ }
334
+
335
+ interface OpenCodeSettings {
336
+ plugin?: string[];
337
+ [key: string]: unknown;
338
+ }
339
+
310
340
  // ── Claude hooks setup ────────────────────────────────────────────────────────
311
341
 
312
342
  const CLAUDE_HOOK_EVENTS = [
@@ -374,7 +404,7 @@ export async function runClaudeHooksInstall(options: GlobalCliOptions): Promise<
374
404
  for (const event of CLAUDE_HOOK_EVENTS) {
375
405
  const existing = updatedHooks[event] ?? [];
376
406
  const alreadyInstalled = existing.some((rule) =>
377
- rule.hooks.some((h) => h.command === hookBin || h.command === "scrylog-claude-hook"),
407
+ rule.hooks.some((h) => typeof h.command === "string" && h.command.includes("scrylog-claude-hook")),
378
408
  );
379
409
  if (!alreadyInstalled) {
380
410
  updatedHooks[event] = [
@@ -386,7 +416,8 @@ export async function runClaudeHooksInstall(options: GlobalCliOptions): Promise<
386
416
 
387
417
  settings.hooks = updatedHooks;
388
418
 
389
- // Write atomically
419
+ // Write atomically with backup
420
+ backupBeforeWrite(settingsPath);
390
421
  const tmp = `${settingsPath}.tmp`;
391
422
  writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
392
423
  renameSync(tmp, settingsPath);
@@ -466,7 +497,7 @@ export async function runCodexHooksInstall(options: GlobalCliOptions): Promise<v
466
497
  for (const event of CODEX_HOOK_EVENTS) {
467
498
  const existing = updatedHooks[event] ?? [];
468
499
  const alreadyInstalled = existing.some((rule) =>
469
- rule.hooks.some((h) => h.command === hookBin || h.command === "scrylog-codex-hook"),
500
+ rule.hooks.some((h) => typeof h.command === "string" && h.command.includes("scrylog-codex-hook")),
470
501
  );
471
502
  if (!alreadyInstalled) {
472
503
  const entry: CodexHookRule = {
@@ -479,7 +510,8 @@ export async function runCodexHooksInstall(options: GlobalCliOptions): Promise<v
479
510
 
480
511
  config.hooks = updatedHooks;
481
512
 
482
- // Write atomically
513
+ // Write atomically with backup
514
+ backupBeforeWrite(hooksPath);
483
515
  const tmp = `${hooksPath}.tmp`;
484
516
  writeFileSync(tmp, `${JSON.stringify(config, null, 2)}\n`, "utf8");
485
517
  renameSync(tmp, hooksPath);
@@ -496,3 +528,43 @@ export async function runCodexHooksInstall(options: GlobalCliOptions): Promise<v
496
528
  process.stdout.write(`Enabled codex.hooksEnabled in scrylog config: ${configPath}\n`);
497
529
  process.stdout.write("Done. Restart the scrylog daemon to apply changes.\n");
498
530
  }
531
+
532
+ // ── OpenCode hooks setup ──────────────────────────────────────────────────────
533
+
534
+ export async function runOpenCodeHooksInstall(options: GlobalCliOptions): Promise<void> {
535
+ const opencodeDir = join(homedir(), ".config", "opencode");
536
+ const settingsPath = join(opencodeDir, "opencode.json");
537
+
538
+ // Load or create opencode.json
539
+ let settings: OpenCodeSettings = {};
540
+ if (existsSync(settingsPath)) {
541
+ try {
542
+ settings = JSON.parse(readFileSync(settingsPath, "utf8")) as OpenCodeSettings;
543
+ } catch {
544
+ process.stderr.write(`Warning: could not parse ${settingsPath} — starting with empty settings.\n`);
545
+ }
546
+ } else {
547
+ mkdirSync(opencodeDir, { recursive: true });
548
+ }
549
+
550
+ // Merge plugin entry without removing existing ones
551
+ const existingPlugins = settings.plugin ?? [];
552
+ const alreadyInstalled = existingPlugins.some(
553
+ (plugin) => typeof plugin === "string" && plugin.includes(OPENCODE_PLUGIN_NAME),
554
+ );
555
+
556
+ if (!alreadyInstalled) {
557
+ settings.plugin = [...existingPlugins, OPENCODE_PLUGIN_NAME];
558
+
559
+ // Write atomically with backup
560
+ backupBeforeWrite(settingsPath);
561
+ const tmp = `${settingsPath}.tmp`;
562
+ writeFileSync(tmp, `${JSON.stringify(settings, null, 2)}\n`, "utf8");
563
+ renameSync(tmp, settingsPath);
564
+ process.stdout.write(`Registered ${OPENCODE_PLUGIN_NAME} in ${settingsPath}\n`);
565
+ } else {
566
+ process.stdout.write(`${OPENCODE_PLUGIN_NAME} is already registered in ${settingsPath}\n`);
567
+ }
568
+
569
+ process.stdout.write("Done. Make sure the plugin is installed globally and restart OpenCode.\n");
570
+ }
package/src/tui/index.ts CHANGED
@@ -8,6 +8,7 @@ import type { ServiceStatus } from "../service/manager";
8
8
  import {
9
9
  runClaudeHooksInstall,
10
10
  runCodexHooksInstall,
11
+ runOpenCodeHooksInstall,
11
12
  runConfigCommand,
12
13
  runDaemonCommand,
13
14
  runDoctor,
@@ -28,6 +29,7 @@ Usage:
28
29
  scrylog token <mcp|api|both> <generate|rotate|clear> [--config <path>]
29
30
  scrylog claude hooks install [--config <path>]
30
31
  scrylog codex hooks install [--config <path>]
32
+ scrylog opencode hooks install [--config <path>]
31
33
  scrylog logs
32
34
 
33
35
  Options:
@@ -40,6 +42,8 @@ Notes:
40
42
  hooks that scrylog uses for real-time session state tracking.
41
43
  'scrylog codex hooks install' configures Codex CLI to emit lifecycle
42
44
  hooks that scrylog uses for real-time session state tracking.
45
+ 'scrylog opencode hooks install' registers the scrylog plugin in
46
+ OpenCode configuration for real-time session state tracking.
43
47
  `);
44
48
  }
45
49
 
@@ -170,6 +174,14 @@ async function main(): Promise<void> {
170
174
  throw new Error(`Unknown codex subcommand: ${subcommand ?? "(none)"}`);
171
175
  }
172
176
 
177
+ if (command === "opencode") {
178
+ if (subcommand === "hooks" && args[2] === "install") {
179
+ await runOpenCodeHooksInstall(options);
180
+ return;
181
+ }
182
+ throw new Error(`Unknown opencode subcommand: ${subcommand ?? "(none)"}`);
183
+ }
184
+
173
185
  throw new Error(`Unknown command: ${command}`);
174
186
  }
175
187