@scrylog/cli 0.1.0 → 0.1.2
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 +8 -3
- package/package.json +8 -6
- package/src/cli/commands.ts +78 -6
- package/src/service/manager.ts +19 -10
- package/src/tui/index.ts +12 -0
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.2",
|
|
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
|
-
"
|
|
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": "
|
|
69
|
+
"zod": "^4.3.6"
|
|
68
70
|
},
|
|
69
71
|
"devDependencies": {
|
|
70
72
|
"@types/blessed": "^0.1.27",
|
|
71
|
-
"@types/bun": "
|
|
72
|
-
"@types/node": "
|
|
73
|
-
"typescript": "
|
|
73
|
+
"@types/bun": "^1.3.14",
|
|
74
|
+
"@types/node": "^24.3.0",
|
|
75
|
+
"typescript": "5.9.3"
|
|
74
76
|
}
|
|
75
77
|
}
|
package/src/cli/commands.ts
CHANGED
|
@@ -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
|
-
|
|
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 ===
|
|
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 ===
|
|
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/service/manager.ts
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
getSystemdUnitPath,
|
|
10
10
|
getUserUid,
|
|
11
11
|
} from "../shared/paths";
|
|
12
|
-
import { IS_LINUX, IS_MAC, IS_WSL } from "../shared/platform";
|
|
12
|
+
import { IS_LINUX, IS_MAC, IS_WSL, whichCommand } from "../shared/platform";
|
|
13
13
|
import { maybeRunCommand } from "../shared/utils";
|
|
14
14
|
|
|
15
15
|
export interface ServiceStatus {
|
|
@@ -52,13 +52,17 @@ function daemonScriptPath(): string {
|
|
|
52
52
|
return join(import.meta.dir, "../daemon/index.ts");
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
function
|
|
55
|
+
export function resolveDaemonExec(configPath?: string, daemonPath = whichCommand("scrylogd")): string[] {
|
|
56
56
|
const resolvedConfigPath = getResolvedConfigPath(configPath);
|
|
57
|
+
if (daemonPath) {
|
|
58
|
+
return [daemonPath, "--config", resolvedConfigPath];
|
|
59
|
+
}
|
|
57
60
|
return [process.execPath, "run", daemonScriptPath(), "--config", resolvedConfigPath];
|
|
58
61
|
}
|
|
59
62
|
|
|
60
63
|
export function renderLaunchAgentPlist(configPath?: string): string {
|
|
61
|
-
const
|
|
64
|
+
const args = resolveDaemonExec(configPath);
|
|
65
|
+
const programArguments = args.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join("\n");
|
|
62
66
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
63
67
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
64
68
|
<plist version="1.0">
|
|
@@ -67,11 +71,7 @@ export function renderLaunchAgentPlist(configPath?: string): string {
|
|
|
67
71
|
<string>${getServiceLabel()}</string>
|
|
68
72
|
<key>ProgramArguments</key>
|
|
69
73
|
<array>
|
|
70
|
-
|
|
71
|
-
<string>${runArg}</string>
|
|
72
|
-
<string>${scriptPath}</string>
|
|
73
|
-
<string>${configFlag}</string>
|
|
74
|
-
<string>${resolvedConfigPath}</string>
|
|
74
|
+
${programArguments}
|
|
75
75
|
</array>
|
|
76
76
|
<key>RunAtLoad</key>
|
|
77
77
|
<true/>
|
|
@@ -83,13 +83,13 @@ export function renderLaunchAgentPlist(configPath?: string): string {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export function renderSystemdUserUnit(configPath?: string): string {
|
|
86
|
-
const
|
|
86
|
+
const args = resolveDaemonExec(configPath);
|
|
87
87
|
return `[Unit]
|
|
88
88
|
Description=scrylog daemon
|
|
89
89
|
After=default.target
|
|
90
90
|
|
|
91
91
|
[Service]
|
|
92
|
-
ExecStart=${
|
|
92
|
+
ExecStart=${args.map(shellQuote).join(" ")}
|
|
93
93
|
Restart=on-failure
|
|
94
94
|
RestartSec=3
|
|
95
95
|
|
|
@@ -240,3 +240,12 @@ class UnsupportedServiceManager implements ServiceManager {
|
|
|
240
240
|
function shellQuote(value: string): string {
|
|
241
241
|
return `'${value.replace(/'/g, `'"'"'`)}'`;
|
|
242
242
|
}
|
|
243
|
+
|
|
244
|
+
function xmlEscape(value: string): string {
|
|
245
|
+
return value
|
|
246
|
+
.replace(/&/g, "&")
|
|
247
|
+
.replace(/</g, "<")
|
|
248
|
+
.replace(/>/g, ">")
|
|
249
|
+
.replace(/"/g, """)
|
|
250
|
+
.replace(/'/g, "'");
|
|
251
|
+
}
|
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
|
|