@scrylog/cli 0.2.0 → 0.3.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.
Files changed (171) hide show
  1. package/README.md +46 -7
  2. package/dist/assets/highlights-eq9cgrbb.scm +604 -0
  3. package/dist/assets/highlights-ghv9g403.scm +205 -0
  4. package/dist/assets/highlights-hk7bwhj4.scm +284 -0
  5. package/dist/assets/highlights-r812a2qc.scm +150 -0
  6. package/dist/assets/highlights-x6tmsnaa.scm +115 -0
  7. package/dist/assets/injections-73j83es3.scm +27 -0
  8. package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
  9. package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
  10. package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
  11. package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
  12. package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
  13. package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
  14. package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
  15. package/dist/cli/assets/providers/SOURCES.md +21 -0
  16. package/dist/cli/assets/providers/claude-terminal.png +0 -0
  17. package/dist/cli/assets/providers/claude-terminal.svg +62 -0
  18. package/dist/cli/assets/providers/claude.png +0 -0
  19. package/dist/cli/assets/providers/copilot.png +0 -0
  20. package/dist/cli/assets/providers/copilot.svg +44 -0
  21. package/dist/cli/assets/providers/openai-terminal.png +0 -0
  22. package/dist/cli/assets/providers/openai-terminal.svg +62 -0
  23. package/dist/cli/assets/providers/openai.png +0 -0
  24. package/dist/cli/assets/providers/opencode-terminal.png +0 -0
  25. package/dist/cli/assets/providers/opencode-terminal.svg +62 -0
  26. package/dist/cli/assets/providers/opencode.png +0 -0
  27. package/dist/cli/assets/providers/shell.png +0 -0
  28. package/dist/cli/assets/providers/shell.svg +23 -0
  29. package/dist/cli/assets/providers/unknown.png +0 -0
  30. package/dist/cli/assets/providers/unknown.svg +23 -0
  31. package/dist/cli/index.js +499 -0
  32. package/dist/cli/index.js.map +61 -0
  33. package/dist/daemon/index.js +670 -0
  34. package/dist/daemon/index.js.map +1475 -0
  35. package/dist/hooks/claude.js +6 -0
  36. package/dist/hooks/claude.js.map +11 -0
  37. package/dist/hooks/codex.js +10 -0
  38. package/dist/hooks/codex.js.map +11 -0
  39. package/dist/mcp/index.js +100 -0
  40. package/dist/mcp/index.js.map +231 -0
  41. package/dist/oc/index.js +21 -0
  42. package/dist/oc/index.js.map +10 -0
  43. package/package.json +26 -20
  44. package/src/postinstall.cjs +11 -0
  45. package/src/api/docs/sessions.keys.ts +0 -6
  46. package/src/api/docs/sessions.ts +0 -81
  47. package/src/api/docs/system.keys.ts +0 -5
  48. package/src/api/docs/system.ts +0 -66
  49. package/src/api/docs/usage.keys.ts +0 -3
  50. package/src/api/docs/usage.ts +0 -24
  51. package/src/api/docs/webhooks.keys.ts +0 -5
  52. package/src/api/docs/webhooks.ts +0 -78
  53. package/src/api/index.ts +0 -18
  54. package/src/api/lib/auth.ts +0 -43
  55. package/src/api/lib/errors.ts +0 -28
  56. package/src/api/lib/route.ts +0 -18
  57. package/src/api/models/claude.ts +0 -4
  58. package/src/api/models/codex.ts +0 -4
  59. package/src/api/models/opencode.ts +0 -11
  60. package/src/api/routes/sessions.ts +0 -47
  61. package/src/api/routes/system.ts +0 -103
  62. package/src/api/routes/usage.ts +0 -15
  63. package/src/api/routes/webhooks/claude.ts +0 -141
  64. package/src/api/routes/webhooks/codex.ts +0 -175
  65. package/src/api/routes/webhooks/opencode.ts +0 -111
  66. package/src/api/routes.ts +0 -93
  67. package/src/api/schemas/common.ts +0 -21
  68. package/src/api/schemas/sessions.ts +0 -72
  69. package/src/api/schemas/usage.ts +0 -50
  70. package/src/api/schemas/webhooks/claude.ts +0 -32
  71. package/src/api/schemas/webhooks/codex.ts +0 -34
  72. package/src/api/schemas/webhooks/opencode.ts +0 -29
  73. package/src/api/services/claude-session.ts +0 -9
  74. package/src/api/services/codex-session.ts +0 -8
  75. package/src/api/services/opencode-session.ts +0 -73
  76. package/src/classifier.ts +0 -206
  77. package/src/cli/commands.ts +0 -632
  78. package/src/cli/prompt.ts +0 -44
  79. package/src/daemon/cli.ts +0 -27
  80. package/src/daemon/collectors/claude/README.md +0 -108
  81. package/src/daemon/collectors/claude/collector.ts +0 -423
  82. package/src/daemon/collectors/claude/discovery.ts +0 -148
  83. package/src/daemon/collectors/claude/hooks.ts +0 -181
  84. package/src/daemon/collectors/claude/index.ts +0 -8
  85. package/src/daemon/collectors/claude/jsonl.ts +0 -85
  86. package/src/daemon/collectors/claude/transcript.ts +0 -246
  87. package/src/daemon/collectors/codex/README.md +0 -147
  88. package/src/daemon/collectors/codex/collector.ts +0 -661
  89. package/src/daemon/collectors/codex/discovery.ts +0 -104
  90. package/src/daemon/collectors/codex/hooks.ts +0 -179
  91. package/src/daemon/collectors/codex/index.ts +0 -7
  92. package/src/daemon/collectors/codex/jsonl.ts +0 -108
  93. package/src/daemon/collectors/codex/transcript.ts +0 -299
  94. package/src/daemon/collectors/git.ts +0 -295
  95. package/src/daemon/collectors/opencode/README.md +0 -106
  96. package/src/daemon/collectors/opencode/collector.ts +0 -545
  97. package/src/daemon/collectors/opencode/discovery.ts +0 -257
  98. package/src/daemon/collectors/opencode/index.ts +0 -5
  99. package/src/daemon/collectors/opencode/session-builder.ts +0 -59
  100. package/src/daemon/collectors/opencode-db.ts +0 -282
  101. package/src/daemon/collectors/process.ts +0 -201
  102. package/src/daemon/collectors/tmux/README.md +0 -128
  103. package/src/daemon/collectors/tmux/collector.ts +0 -472
  104. package/src/daemon/collectors/tmux/enrichment.ts +0 -241
  105. package/src/daemon/collectors/tmux/index.ts +0 -4
  106. package/src/daemon/collectors/tmux/parse.ts +0 -128
  107. package/src/daemon/collectors/usage.ts +0 -193
  108. package/src/daemon/index.ts +0 -207
  109. package/src/daemon/scheduler.ts +0 -86
  110. package/src/daemon/store.ts +0 -588
  111. package/src/detect.ts +0 -143
  112. package/src/hooks/claude.ts +0 -596
  113. package/src/hooks/codex.ts +0 -517
  114. package/src/legacy/classifier.ts +0 -215
  115. package/src/legacy/detect.ts +0 -143
  116. package/src/mcp/client-server.ts +0 -21
  117. package/src/mcp/http-handler.ts +0 -126
  118. package/src/mcp/index.ts +0 -61
  119. package/src/mcp/resources/config-current.ts +0 -19
  120. package/src/mcp/resources/index.ts +0 -11
  121. package/src/mcp/resources/sessions-active.ts +0 -19
  122. package/src/mcp/resources/sessions-all.ts +0 -19
  123. package/src/mcp/schemas/sessions.ts +0 -3
  124. package/src/mcp/server.ts +0 -17
  125. package/src/mcp/services/api-adapter.ts +0 -48
  126. package/src/mcp/services/data-source.ts +0 -22
  127. package/src/mcp/services/store-adapter.ts +0 -40
  128. package/src/mcp/tools/get-active-sessions.ts +0 -16
  129. package/src/mcp/tools/get-session-children.ts +0 -24
  130. package/src/mcp/tools/get-session-messages.ts +0 -24
  131. package/src/mcp/tools/get-session.ts +0 -23
  132. package/src/mcp/tools/index.ts +0 -15
  133. package/src/mcp/tools/list-sessions.ts +0 -16
  134. package/src/neo-blessed.d.ts +0 -4
  135. package/src/oc/index.ts +0 -302
  136. package/src/service/manager.ts +0 -284
  137. package/src/shared/config.ts +0 -478
  138. package/src/shared/ipc.ts +0 -80
  139. package/src/shared/logger.ts +0 -248
  140. package/src/shared/paths.ts +0 -73
  141. package/src/shared/platform.ts +0 -110
  142. package/src/shared/session-view.ts +0 -241
  143. package/src/shared/types.ts +0 -299
  144. package/src/shared/utils.ts +0 -339
  145. package/src/tui/app.ts +0 -920
  146. package/src/tui/client.ts +0 -149
  147. package/src/tui/index.ts +0 -217
  148. package/src/types.ts +0 -113
  149. package/src/usage/README.md +0 -323
  150. package/src/usage/api.ts +0 -99
  151. package/src/usage/client.ts +0 -3
  152. package/src/usage/clients/anthropic-oauth-usage.ts +0 -178
  153. package/src/usage/clients/anthropic-web-usage.ts +0 -245
  154. package/src/usage/clients/claude-cli-probe.ts +0 -187
  155. package/src/usage/clients/codex-oauth-usage.ts +0 -113
  156. package/src/usage/clients/opencode-go-usage.ts +0 -441
  157. package/src/usage/index.ts +0 -20
  158. package/src/usage/mappers.ts +0 -273
  159. package/src/usage/planner.ts +0 -54
  160. package/src/usage/providers/claude.ts +0 -113
  161. package/src/usage/providers/codex.ts +0 -34
  162. package/src/usage/providers/opencode-go.ts +0 -37
  163. package/src/usage/resolver.ts +0 -4
  164. package/src/usage/resolvers/browser-cookies.ts +0 -470
  165. package/src/usage/resolvers/claude-auth.ts +0 -108
  166. package/src/usage/resolvers/claude-cli.ts +0 -24
  167. package/src/usage/resolvers/claude-desktop-auth.ts +0 -160
  168. package/src/usage/resolvers/claude-web.ts +0 -16
  169. package/src/usage/resolvers/codex-auth.ts +0 -117
  170. package/src/usage/resolvers/opencode-go-web.ts +0 -14
  171. package/src/usage/types.ts +0 -190
@@ -1,48 +0,0 @@
1
- import { flattenSessionTree } from "../../shared/session-view";
2
- import type { SessionTreeItem, SessionListItem } from "../../shared/session-view";
3
- import type { DataSource } from "./data-source";
4
-
5
- type FetchFn = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
6
-
7
- export function createApiAdapter(daemonFetch: FetchFn): DataSource {
8
- return {
9
- fetchSessions: async () => {
10
- const response = await daemonFetch("/api/sessions");
11
- const data = (await response.json()) as { sessions: SessionTreeItem[] };
12
- return data.sessions;
13
- },
14
-
15
- fetchSession: async (id) => {
16
- const response = await daemonFetch(`/api/sessions/${encodeURIComponent(id)}`);
17
- if (response.status === 404) return null;
18
- const data = (await response.json()) as { session: unknown };
19
- return data.session as any;
20
- },
21
-
22
- fetchSessionChildren: async (id) => {
23
- const response = await daemonFetch(`/api/sessions/${encodeURIComponent(id)}/children`);
24
- if (response.status === 404) return [];
25
- const data = (await response.json()) as { children: SessionListItem[] };
26
- return data.children;
27
- },
28
-
29
- fetchSessionMessages: async (id) => {
30
- const response = await daemonFetch(`/api/sessions/${encodeURIComponent(id)}/messages`);
31
- if (response.status === 404) return { sessionId: id, messages: [] };
32
- return await response.json();
33
- },
34
-
35
- fetchActiveSessions: async () => {
36
- const response = await daemonFetch("/api/sessions");
37
- const data = (await response.json()) as { sessions: SessionTreeItem[] };
38
- return flattenSessionTree(data.sessions).filter(
39
- (s) => s.state === "working" || s.state === "waiting-input",
40
- );
41
- },
42
-
43
- fetchConfig: async () => {
44
- const response = await daemonFetch("/api/config");
45
- return await response.json();
46
- },
47
- };
48
- }
@@ -1,22 +0,0 @@
1
- import type { SessionTreeItem, SessionListItem } from "../../shared/session-view";
2
- import type { SessionModel } from "../../shared/types";
3
-
4
- /**
5
- * Uniform data-access interface for MCP tools and resources.
6
- * Both the in-daemon server and the stdio binary implement this
7
- * via StoreAdapter and ApiAdapter respectively.
8
- */
9
- export interface DataSource {
10
- /** Fetch the full visible session tree (roots with children arrays). */
11
- fetchSessions(): Promise<SessionTreeItem[]>;
12
- /** Fetch a single session by ID, or null if not found. */
13
- fetchSession(id: string): Promise<SessionModel | null>;
14
- /** Fetch active children of a parent session. */
15
- fetchSessionChildren(id: string): Promise<SessionListItem[]>;
16
- /** Fetch recent messages for a session (returns messages payload or empty object). */
17
- fetchSessionMessages(id: string): Promise<unknown>;
18
- /** Fetch all active (working / waiting-input) sessions flattened. */
19
- fetchActiveSessions(): Promise<SessionListItem[]>;
20
- /** Fetch current daemon configuration (sensitive fields redacted). */
21
- fetchConfig(): Promise<unknown>;
22
- }
@@ -1,40 +0,0 @@
1
- import type { DaemonStore } from "../../daemon/store";
2
- import type { ScrylogConfig } from "../../shared/config";
3
- import { buildSessionView, flattenSessionTree } from "../../shared/session-view";
4
- import type { DataSource } from "./data-source";
5
-
6
- export function createStoreAdapter(store: DaemonStore, config: ScrylogConfig): DataSource {
7
- function getSessionView() {
8
- return buildSessionView(store.getSessions(), config);
9
- }
10
-
11
- return {
12
- fetchSessions: async () => getSessionView().sessions,
13
-
14
- fetchSession: async (id) => store.getSession(id) ?? null,
15
-
16
- fetchSessionChildren: async (id) => {
17
- const parent = store.getSession(id);
18
- if (!parent) return [];
19
- return getSessionView().sessions.find((s) => s.id === id)?.children ?? [];
20
- },
21
-
22
- fetchSessionMessages: async (id) => {
23
- const session = store.getSession(id);
24
- if (!session) return { sessionId: id, messages: [] };
25
- return { sessionId: id, messages: [] };
26
- },
27
-
28
- fetchActiveSessions: async () => {
29
- return flattenSessionTree(getSessionView().sessions).filter(
30
- (s) => s.state === "working" || s.state === "waiting-input",
31
- );
32
- },
33
-
34
- fetchConfig: async () => ({
35
- ...config,
36
- api: { ...config.api, bearerToken: undefined },
37
- mcp: { ...config.mcp, bearerToken: undefined },
38
- }),
39
- };
40
- }
@@ -1,16 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import type { DataSource } from "../services/data-source";
3
-
4
- export function registerGetActiveSessions(server: McpServer, ds: DataSource) {
5
- server.tool(
6
- "get_active_sessions",
7
- "List sessions that are currently in a working or waiting-input state",
8
- {},
9
- async () => {
10
- const sessions = await ds.fetchActiveSessions();
11
- return {
12
- content: [{ type: "text" as const, text: JSON.stringify(sessions, null, 2) }],
13
- };
14
- },
15
- );
16
- }
@@ -1,24 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { SessionIdSchema } from "../schemas/sessions";
3
- import type { DataSource } from "../services/data-source";
4
-
5
- export function registerGetSessionChildren(server: McpServer, ds: DataSource) {
6
- server.tool(
7
- "get_session_children",
8
- "Get all active sub-agent sessions spawned by a parent session, with full detail",
9
- { id: SessionIdSchema },
10
- async ({ id }) => {
11
- const session = await ds.fetchSession(id);
12
- if (!session) {
13
- return {
14
- isError: true,
15
- content: [{ type: "text" as const, text: `Session '${id}' not found` }],
16
- };
17
- }
18
- const children = await ds.fetchSessionChildren(id);
19
- return {
20
- content: [{ type: "text" as const, text: JSON.stringify({ sessionId: id, children }, null, 2) }],
21
- };
22
- },
23
- );
24
- }
@@ -1,24 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { SessionIdSchema } from "../schemas/sessions";
3
- import type { DataSource } from "../services/data-source";
4
-
5
- export function registerGetSessionMessages(server: McpServer, ds: DataSource) {
6
- server.tool(
7
- "get_session_messages",
8
- "Get recent messages for a session (if available from Claude transcript or OpenCode SDK)",
9
- { id: SessionIdSchema },
10
- async ({ id }) => {
11
- const session = await ds.fetchSession(id);
12
- if (!session) {
13
- return {
14
- isError: true,
15
- content: [{ type: "text" as const, text: `Session '${id}' not found` }],
16
- };
17
- }
18
- const messages = await ds.fetchSessionMessages(id);
19
- return {
20
- content: [{ type: "text" as const, text: JSON.stringify(messages, null, 2) }],
21
- };
22
- },
23
- );
24
- }
@@ -1,23 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { SessionIdSchema } from "../schemas/sessions";
3
- import type { DataSource } from "../services/data-source";
4
-
5
- export function registerGetSession(server: McpServer, ds: DataSource) {
6
- server.tool(
7
- "get_session",
8
- "Get details for a specific session by ID",
9
- { id: SessionIdSchema },
10
- async ({ id }) => {
11
- const session = await ds.fetchSession(id);
12
- if (!session) {
13
- return {
14
- isError: true,
15
- content: [{ type: "text" as const, text: `Session '${id}' not found` }],
16
- };
17
- }
18
- return {
19
- content: [{ type: "text" as const, text: JSON.stringify(session, null, 2) }],
20
- };
21
- },
22
- );
23
- }
@@ -1,15 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import type { DataSource } from "../services/data-source";
3
- import { registerListSessions } from "./list-sessions";
4
- import { registerGetActiveSessions } from "./get-active-sessions";
5
- import { registerGetSession } from "./get-session";
6
- import { registerGetSessionMessages } from "./get-session-messages";
7
- import { registerGetSessionChildren } from "./get-session-children";
8
-
9
- export function registerAllTools(server: McpServer, ds: DataSource) {
10
- registerListSessions(server, ds);
11
- registerGetActiveSessions(server, ds);
12
- registerGetSession(server, ds);
13
- registerGetSessionMessages(server, ds);
14
- registerGetSessionChildren(server, ds);
15
- }
@@ -1,16 +0,0 @@
1
- import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import type { DataSource } from "../services/data-source";
3
-
4
- export function registerListSessions(server: McpServer, ds: DataSource) {
5
- server.tool(
6
- "list_sessions",
7
- "List all monitored AI coding sessions as a tree. Root sessions include a 'children' array of active sub-agent sessions.",
8
- {},
9
- async () => {
10
- const sessions = await ds.fetchSessions();
11
- return {
12
- content: [{ type: "text" as const, text: JSON.stringify(sessions, null, 2) }],
13
- };
14
- },
15
- );
16
- }
@@ -1,4 +0,0 @@
1
- declare module "neo-blessed" {
2
- import blessed = require("blessed");
3
- export = blessed;
4
- }
package/src/oc/index.ts DELETED
@@ -1,302 +0,0 @@
1
- #!/usr/bin/env bun
2
- /**
3
- * oc — OpenCode launcher / attach helper
4
- *
5
- * Usage:
6
- * oc [args...] Auto-discover running instance → attach or launch
7
- * oc [args...] All extra args are forwarded to the opencode command
8
- *
9
- * Environment variables:
10
- * OPENCODE_LOCAL_PORT Force a specific port (skips discovery)
11
- * OPENCODE_HOST Force a specific host (skips discovery, default 127.0.0.1)
12
- *
13
- * When both env vars are absent, oc discovers running opencode processes via
14
- * process scanning (ps on Unix, wmic on Windows), probes their health endpoints,
15
- * and either attaches to a live instance or launches a new one on port 4096.
16
- *
17
- * If multiple live instances are found an interactive numbered selector is shown.
18
- */
19
-
20
- const DEFAULT_PORT = 4096;
21
- const DEFAULT_HOST = "127.0.0.1";
22
- const HEALTH_TIMEOUT_MS = 800;
23
-
24
- function printHelp() {
25
- process.stdout.write(`oc
26
-
27
- Usage:
28
- oc [opencode args...]
29
-
30
- Environment variables:
31
- OPENCODE_LOCAL_PORT Force a specific port
32
- OPENCODE_HOST Force a specific host
33
-
34
- Behavior:
35
- Auto-discovers running OpenCode instances and attaches when possible.
36
- Launches a new instance on port 4096 when none is available.
37
- `);
38
- }
39
-
40
- // ── Types ─────────────────────────────────────────────────────────────────────
41
-
42
- interface Candidate {
43
- url: string;
44
- pid?: number;
45
- }
46
-
47
- // ── Process scanning ──────────────────────────────────────────────────────────
48
-
49
- function scanProcessesUnix(): Candidate[] {
50
- const proc = Bun.spawnSync(["ps", "-ax", "-o", "pid=,command="], {
51
- stdout: "pipe",
52
- stderr: "pipe",
53
- });
54
- if (proc.exitCode !== 0) return [];
55
-
56
- const output = Buffer.from(proc.stdout).toString("utf8");
57
- return parseProcessLines(output, /^\s*(\d+)\s+(.*)$/);
58
- }
59
-
60
- function scanProcessesWindows(): Candidate[] {
61
- const proc = Bun.spawnSync(
62
- ["wmic", "process", "get", "ProcessId,CommandLine", "/format:csv"],
63
- { stdout: "pipe", stderr: "pipe" },
64
- );
65
- if (proc.exitCode !== 0) return [];
66
-
67
- const output = Buffer.from(proc.stdout).toString("utf8");
68
- const candidates: Candidate[] = [];
69
-
70
- for (const line of output.split(/\r?\n/).filter(Boolean)) {
71
- // CSV columns: Node,CommandLine,ProcessId
72
- const cols = line.split(",");
73
- if (cols.length < 3) continue;
74
- const command = cols.slice(1, -1).join(","); // CommandLine may contain commas
75
- const pidStr = cols[cols.length - 1] ?? "";
76
- const pid = Number.parseInt(pidStr.trim(), 10);
77
- if (Number.isNaN(pid)) continue;
78
-
79
- const candidate = extractFromCommand(command, pid);
80
- if (candidate) candidates.push(candidate);
81
- }
82
-
83
- return candidates;
84
- }
85
-
86
- function parseProcessLines(
87
- output: string,
88
- linePattern: RegExp,
89
- ): Candidate[] {
90
- const candidates: Candidate[] = [];
91
-
92
- for (const line of output.split(/\r?\n/).filter(Boolean)) {
93
- const match = line.match(linePattern);
94
- if (!match) continue;
95
- const pid = Number.parseInt(match[1] ?? "", 10);
96
- const command = match[2] ?? "";
97
- const candidate = extractFromCommand(command, pid);
98
- if (candidate) candidates.push(candidate);
99
- }
100
-
101
- return candidates;
102
- }
103
-
104
- function extractFromCommand(command: string, pid: number): Candidate | null {
105
- const isServe = /\bopencode\b.*\bserve\b/i.test(command);
106
- const isTuiWithPort =
107
- /\bopencode\b/i.test(command) &&
108
- /--port[=\s]+([1-9]\d*)/.test(command) &&
109
- !/\bserve\b/i.test(command);
110
-
111
- if (!isServe && !isTuiWithPort) return null;
112
-
113
- const portMatch = command.match(/--port[=\s]+(\d+)/);
114
- const hostMatch = command.match(/--hostname[=\s]+(\S+)/);
115
- const port = portMatch ? Number.parseInt(portMatch[1] ?? "", 10) : DEFAULT_PORT;
116
- const rawHost = hostMatch ? (hostMatch[1] ?? DEFAULT_HOST) : DEFAULT_HOST;
117
- // 0.0.0.0 listens on all interfaces — connect via loopback
118
- const host = rawHost === "0.0.0.0" ? "127.0.0.1" : rawHost;
119
-
120
- return { url: `http://${host}:${port}`, pid };
121
- }
122
-
123
- function discoverCandidates(): Candidate[] {
124
- const raw =
125
- process.platform === "win32" ? scanProcessesWindows() : scanProcessesUnix();
126
-
127
- // Deduplicate by URL
128
- const seen = new Set<string>();
129
- const deduped = raw.filter((c) => {
130
- if (seen.has(c.url)) return false;
131
- seen.add(c.url);
132
- return true;
133
- });
134
-
135
- // Always include the default endpoint as a fallback
136
- if (!seen.has(`http://${DEFAULT_HOST}:${DEFAULT_PORT}`)) {
137
- deduped.push({ url: `http://${DEFAULT_HOST}:${DEFAULT_PORT}` });
138
- }
139
-
140
- return deduped;
141
- }
142
-
143
- // ── Health probe ──────────────────────────────────────────────────────────────
144
-
145
- async function isAlive(url: string): Promise<boolean> {
146
- try {
147
- const res = await fetch(`${url}/global/health`, {
148
- signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS),
149
- });
150
- return res.ok;
151
- } catch {
152
- return false;
153
- }
154
- }
155
-
156
- async function filterAlive(candidates: Candidate[]): Promise<Candidate[]> {
157
- const results = await Promise.all(
158
- candidates.map(async (c) => ({ candidate: c, alive: await isAlive(c.url) })),
159
- );
160
- return results.filter((r) => r.alive).map((r) => r.candidate);
161
- }
162
-
163
- // ── Interactive selector ──────────────────────────────────────────────────────
164
-
165
- async function selectInstance(candidates: Candidate[]): Promise<Candidate> {
166
- const lines = [
167
- `Found ${candidates.length} running OpenCode instances:`,
168
- ...candidates.map(
169
- (c, i) =>
170
- ` [${i + 1}] ${c.url}${c.pid !== undefined ? ` (pid ${c.pid})` : ""}`,
171
- ),
172
- `Select [1-${candidates.length}]: `,
173
- ];
174
-
175
- process.stderr.write(lines.join("\n"));
176
-
177
- // Read a line from stdin
178
- for (let attempt = 0; attempt < 3; attempt++) {
179
- const raw = await readLine();
180
- const n = Number.parseInt(raw.trim(), 10);
181
- if (!Number.isNaN(n) && n >= 1 && n <= candidates.length) {
182
- return candidates[n - 1]!;
183
- }
184
- if (attempt < 2) {
185
- process.stderr.write(`Invalid choice. Select [1-${candidates.length}]: `);
186
- }
187
- }
188
-
189
- process.stderr.write("\nInvalid selection. Exiting.\n");
190
- process.exit(1);
191
- }
192
-
193
- function readLine(): Promise<string> {
194
- return new Promise((resolve) => {
195
- let buffer = "";
196
- process.stdin.setEncoding("utf8");
197
- process.stdin.resume();
198
- process.stdin.once("data", (chunk: string) => {
199
- process.stdin.pause();
200
- buffer += chunk;
201
- resolve(buffer.split("\n")[0] ?? "");
202
- });
203
- });
204
- }
205
-
206
- // ── Spawn helpers ─────────────────────────────────────────────────────────────
207
-
208
- async function run(cmd: string[]): Promise<void> {
209
- const proc = Bun.spawn(cmd, {
210
- stdio: ["inherit", "inherit", "inherit"],
211
- });
212
- const code = await proc.exited;
213
- process.exit(code ?? 0);
214
- }
215
-
216
- function hasDirFlag(args: string[]): boolean {
217
- return args.includes("--dir") || args.some((arg) => arg.startsWith("--dir="));
218
- }
219
-
220
- function hasProjectArg(args: string[]): boolean {
221
- const first = args[0];
222
- return first !== undefined && !first.startsWith("-");
223
- }
224
-
225
- export function buildAttachCommand(url: string, extraArgs: string[], cwd: string): string[] {
226
- return hasDirFlag(extraArgs)
227
- ? ["opencode", "attach", url, ...extraArgs]
228
- : ["opencode", "attach", url, "--dir", cwd, ...extraArgs];
229
- }
230
-
231
- export function buildLaunchCommand(extraArgs: string[], cwd: string, port?: string, host?: string): string[] {
232
- const args = hasProjectArg(extraArgs) ? [...extraArgs] : [cwd, ...extraArgs];
233
- const command = ["opencode", ...args];
234
- if (port !== undefined) command.push("--port", port);
235
- if (host !== undefined) command.push("--hostname", host);
236
- return command;
237
- }
238
-
239
- // ── Main ──────────────────────────────────────────────────────────────────────
240
-
241
- async function main(): Promise<void> {
242
- const extraArgs = process.argv.slice(2);
243
- if (extraArgs.includes("--help") || extraArgs.includes("-h")) {
244
- printHelp();
245
- process.exit(0);
246
- }
247
- const cwd = process.cwd();
248
-
249
- const envPort = process.env["OPENCODE_LOCAL_PORT"];
250
- const envHost = process.env["OPENCODE_HOST"];
251
-
252
- // ── Explicit mode: both env vars set ───────────────────────────────────────
253
- if (envPort !== undefined && envHost !== undefined) {
254
- const url = `http://${envHost}:${envPort}`;
255
- const alive = await isAlive(url);
256
- if (alive) {
257
- await run(buildAttachCommand(url, extraArgs, cwd));
258
- } else {
259
- await run(buildLaunchCommand(extraArgs, cwd, envPort, envHost));
260
- }
261
- return;
262
- }
263
-
264
- // Partial override: only port or only host specified
265
- if (envPort !== undefined || envHost !== undefined) {
266
- const port = envPort ?? String(DEFAULT_PORT);
267
- const host = envHost ?? DEFAULT_HOST;
268
- const url = `http://${host}:${port}`;
269
- const alive = await isAlive(url);
270
- if (alive) {
271
- await run(buildAttachCommand(url, extraArgs, cwd));
272
- } else {
273
- await run(buildLaunchCommand(extraArgs, cwd, port, host));
274
- }
275
- return;
276
- }
277
-
278
- // ── Auto-discovery mode ────────────────────────────────────────────────────
279
- const candidates = discoverCandidates();
280
- const alive = await filterAlive(candidates);
281
-
282
- if (alive.length === 0) {
283
- // Nothing running — launch on default port
284
- await run(buildLaunchCommand(extraArgs, cwd, String(DEFAULT_PORT)));
285
- return;
286
- }
287
-
288
- if (alive.length === 1) {
289
- await run(buildAttachCommand(alive[0]!.url, extraArgs, cwd));
290
- return;
291
- }
292
-
293
- // Multiple instances — show selector
294
- const chosen = await selectInstance(alive);
295
- await run(buildAttachCommand(chosen.url, extraArgs, cwd));
296
- }
297
-
298
- if (import.meta.main) {
299
- await main();
300
- }
301
-
302
- export {};