@scrylog/cli 0.3.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.
- package/dist/assets/highlights-eq9cgrbb.scm +604 -0
- package/dist/assets/highlights-ghv9g403.scm +205 -0
- package/dist/assets/highlights-hk7bwhj4.scm +284 -0
- package/dist/assets/highlights-r812a2qc.scm +150 -0
- package/dist/assets/highlights-x6tmsnaa.scm +115 -0
- package/dist/assets/injections-73j83es3.scm +27 -0
- package/dist/assets/parser.worker-xyhf83rf.js +4563 -0
- package/dist/assets/tree-sitter-3jzf13jk.wasm +0 -0
- package/dist/assets/tree-sitter-javascript-nd0q4pe9.wasm +0 -0
- package/dist/assets/tree-sitter-markdown-411r6y9b.wasm +0 -0
- package/dist/assets/tree-sitter-markdown_inline-j5349f42.wasm +0 -0
- package/dist/assets/tree-sitter-typescript-zxjzwt75.wasm +0 -0
- package/dist/assets/tree-sitter-zig-e78zbjpm.wasm +0 -0
- package/dist/cli/index.js +499 -0
- package/dist/cli/index.js.map +61 -0
- package/dist/daemon/index.js +670 -0
- package/dist/daemon/index.js.map +1475 -0
- package/dist/hooks/claude.js +6 -0
- package/dist/hooks/claude.js.map +11 -0
- package/dist/hooks/codex.js +10 -0
- package/dist/hooks/codex.js.map +11 -0
- package/dist/mcp/index.js +100 -0
- package/dist/mcp/index.js.map +231 -0
- package/dist/oc/index.js +21 -0
- package/dist/oc/index.js.map +10 -0
- package/package.json +16 -13
- package/src/api/docs/sessions.keys.ts +0 -6
- package/src/api/docs/sessions.ts +0 -82
- package/src/api/docs/system.keys.ts +0 -5
- package/src/api/docs/system.ts +0 -69
- package/src/api/docs/usage.keys.ts +0 -3
- package/src/api/docs/usage.ts +0 -24
- package/src/api/docs/webhooks.keys.ts +0 -5
- package/src/api/docs/webhooks.ts +0 -78
- package/src/api/index.ts +0 -18
- package/src/api/lib/auth.ts +0 -43
- package/src/api/lib/errors.ts +0 -28
- package/src/api/lib/route.ts +0 -18
- package/src/api/models/claude.ts +0 -4
- package/src/api/models/codex.ts +0 -4
- package/src/api/models/opencode.ts +0 -11
- package/src/api/routes/sessions.ts +0 -51
- package/src/api/routes/system.ts +0 -106
- package/src/api/routes/usage.ts +0 -15
- package/src/api/routes/webhooks/claude.ts +0 -149
- package/src/api/routes/webhooks/codex.ts +0 -175
- package/src/api/routes/webhooks/opencode.ts +0 -112
- package/src/api/routes.ts +0 -93
- package/src/api/schemas/common.ts +0 -29
- package/src/api/schemas/sessions.ts +0 -86
- package/src/api/schemas/usage.ts +0 -51
- package/src/api/schemas/webhooks/claude.ts +0 -32
- package/src/api/schemas/webhooks/codex.ts +0 -34
- package/src/api/schemas/webhooks/opencode.ts +0 -29
- package/src/api/services/claude-session.ts +0 -9
- package/src/api/services/codex-session.ts +0 -8
- package/src/api/services/opencode-session.ts +0 -73
- package/src/classifier.ts +0 -206
- package/src/cli/commands.ts +0 -714
- package/src/cli/index.ts +0 -18
- package/src/cli/program.ts +0 -175
- package/src/cli/prompt.ts +0 -44
- package/src/daemon/cli.ts +0 -27
- package/src/daemon/collectors/claude/README.md +0 -108
- package/src/daemon/collectors/claude/collector.ts +0 -423
- package/src/daemon/collectors/claude/discovery.ts +0 -127
- package/src/daemon/collectors/claude/hooks.ts +0 -181
- package/src/daemon/collectors/claude/index.ts +0 -8
- package/src/daemon/collectors/claude/jsonl.ts +0 -85
- package/src/daemon/collectors/claude/transcript.ts +0 -246
- package/src/daemon/collectors/codex/README.md +0 -147
- package/src/daemon/collectors/codex/collector.ts +0 -661
- package/src/daemon/collectors/codex/discovery.ts +0 -104
- package/src/daemon/collectors/codex/hooks.ts +0 -179
- package/src/daemon/collectors/codex/index.ts +0 -7
- package/src/daemon/collectors/codex/jsonl.ts +0 -108
- package/src/daemon/collectors/codex/transcript.ts +0 -299
- package/src/daemon/collectors/git.ts +0 -353
- package/src/daemon/collectors/herdr/collector.ts +0 -128
- package/src/daemon/collectors/herdr/index.ts +0 -2
- package/src/daemon/collectors/herdr/normalization.ts +0 -111
- package/src/daemon/collectors/opencode/README.md +0 -106
- package/src/daemon/collectors/opencode/collector.ts +0 -545
- package/src/daemon/collectors/opencode/discovery.ts +0 -257
- package/src/daemon/collectors/opencode/index.ts +0 -5
- package/src/daemon/collectors/opencode/session-builder.ts +0 -59
- package/src/daemon/collectors/opencode-db.ts +0 -294
- package/src/daemon/collectors/process.ts +0 -201
- package/src/daemon/collectors/tmux/README.md +0 -128
- package/src/daemon/collectors/tmux/collector.ts +0 -492
- package/src/daemon/collectors/tmux/enrichment.ts +0 -242
- package/src/daemon/collectors/tmux/index.ts +0 -4
- package/src/daemon/collectors/tmux/parse.ts +0 -128
- package/src/daemon/collectors/usage.ts +0 -259
- package/src/daemon/index.ts +0 -209
- package/src/daemon/scheduler.ts +0 -86
- package/src/daemon/store.ts +0 -630
- package/src/detect.ts +0 -143
- package/src/hooks/claude.ts +0 -596
- package/src/hooks/codex.ts +0 -517
- package/src/legacy/classifier.ts +0 -215
- package/src/legacy/detect.ts +0 -143
- package/src/mcp/client-server.ts +0 -21
- package/src/mcp/http-handler.ts +0 -126
- package/src/mcp/index.ts +0 -61
- package/src/mcp/resources/config-current.ts +0 -19
- package/src/mcp/resources/index.ts +0 -11
- package/src/mcp/resources/sessions-active.ts +0 -19
- package/src/mcp/resources/sessions-all.ts +0 -19
- package/src/mcp/schemas/sessions.ts +0 -3
- package/src/mcp/server.ts +0 -17
- package/src/mcp/services/api-adapter.ts +0 -48
- package/src/mcp/services/data-source.ts +0 -22
- package/src/mcp/services/store-adapter.ts +0 -40
- package/src/mcp/tools/get-active-sessions.ts +0 -16
- package/src/mcp/tools/get-session-children.ts +0 -24
- package/src/mcp/tools/get-session-messages.ts +0 -24
- package/src/mcp/tools/get-session.ts +0 -23
- package/src/mcp/tools/index.ts +0 -15
- package/src/mcp/tools/list-sessions.ts +0 -16
- package/src/oc/index.ts +0 -302
- package/src/service/manager.ts +0 -284
- package/src/shared/config.ts +0 -511
- package/src/shared/ipc.ts +0 -80
- package/src/shared/logger.ts +0 -248
- package/src/shared/paths.ts +0 -73
- package/src/shared/platform.ts +0 -110
- package/src/shared/session-exclusions.ts +0 -5
- package/src/shared/session-view.ts +0 -302
- package/src/shared/terminal-capabilities.ts +0 -130
- package/src/shared/types.ts +0 -330
- package/src/shared/utils.ts +0 -339
- package/src/shared/version.ts +0 -44
- package/src/tui/App.tsx +0 -240
- package/src/tui/client.ts +0 -163
- package/src/tui/components/BindingsModal.tsx +0 -82
- package/src/tui/components/DashboardHeader.tsx +0 -256
- package/src/tui/components/KeyBar.tsx +0 -67
- package/src/tui/components/SessionDetailModal.tsx +0 -169
- package/src/tui/components/SessionsView.tsx +0 -184
- package/src/tui/components/TabBar.tsx +0 -40
- package/src/tui/components/UsageSummaryPane.tsx +0 -272
- package/src/tui/components/UsageView.tsx +0 -242
- package/src/tui/components/tool-icons.ts +0 -24
- package/src/tui/index.tsx +0 -106
- package/src/tui/keybindings.ts +0 -49
- package/src/tui/model.ts +0 -295
- package/src/tui/state.ts +0 -77
- package/src/tui/theme.ts +0 -110
- package/src/types.ts +0 -113
- package/src/usage/README.md +0 -323
- package/src/usage/api.ts +0 -99
- package/src/usage/client.ts +0 -3
- package/src/usage/clients/anthropic-oauth-usage.ts +0 -258
- package/src/usage/clients/anthropic-web-usage.ts +0 -329
- package/src/usage/clients/claude-cli-probe.ts +0 -513
- package/src/usage/clients/codex-app-server.ts +0 -354
- package/src/usage/clients/codex-oauth-usage.ts +0 -240
- package/src/usage/clients/opencode-go-api.ts +0 -100
- package/src/usage/clients/opencode-go-usage.ts +0 -577
- package/src/usage/errors.ts +0 -33
- package/src/usage/index.ts +0 -20
- package/src/usage/local/opencode-go-local.ts +0 -194
- package/src/usage/mappers.ts +0 -282
- package/src/usage/planner.ts +0 -118
- package/src/usage/providers/claude.ts +0 -233
- package/src/usage/providers/codex.ts +0 -115
- package/src/usage/providers/opencode-go.ts +0 -115
- package/src/usage/resolver.ts +0 -4
- package/src/usage/resolvers/browser-cookies.ts +0 -808
- package/src/usage/resolvers/claude-auth.ts +0 -208
- package/src/usage/resolvers/claude-cli.ts +0 -24
- package/src/usage/resolvers/claude-desktop-auth.ts +0 -170
- package/src/usage/resolvers/claude-web.ts +0 -82
- package/src/usage/resolvers/codex-auth.ts +0 -174
- package/src/usage/resolvers/keychain-coordinator.ts +0 -397
- package/src/usage/resolvers/opencode-go-web.ts +0 -55
- package/src/usage/types.ts +0 -225
- package/src/usage/web-session-cache.ts +0 -37
- /package/{src/tui → dist/cli}/assets/providers/SOURCES.md +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/claude.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/copilot.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/openai.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode-terminal.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/opencode.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/shell.svg +0 -0
- /package/{src/tui → dist/cli}/assets/providers/unknown.png +0 -0
- /package/{src/tui → dist/cli}/assets/providers/unknown.svg +0 -0
|
@@ -1,242 +0,0 @@
|
|
|
1
|
-
import type { SessionModel, TmuxPaneRecord, PaneSample } from "../../../shared/types";
|
|
2
|
-
import type { OpenCodeConfig } from "../../../shared/config";
|
|
3
|
-
import type { DaemonStore } from "../../store";
|
|
4
|
-
import { classifyPane } from "../../../legacy/classifier";
|
|
5
|
-
import { parseOpencodeUrl } from "../opencode/discovery";
|
|
6
|
-
|
|
7
|
-
export function isOpenCodeManagedSession(session: SessionModel): boolean {
|
|
8
|
-
return session.detectionSource === "opencode-sdk"
|
|
9
|
-
|| session.detectionSource === "opencode-hook"
|
|
10
|
-
|| session.detectionSource === "opencode-db";
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function isClaudeManagedSession(session: SessionModel): boolean {
|
|
14
|
-
return session.tool === "claude" && (
|
|
15
|
-
session.detectionSource === "claude-transcript" ||
|
|
16
|
-
session.detectionSource === "claude-hook"
|
|
17
|
-
);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function isCodexManagedSession(session: SessionModel): boolean {
|
|
21
|
-
return session.tool === "codex" && (
|
|
22
|
-
session.detectionSource === "codex-transcript" ||
|
|
23
|
-
session.detectionSource === "codex-hook"
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function clearStaleClaudeTmuxBindings(store: DaemonStore, livePaneIds: ReadonlySet<string>): void {
|
|
28
|
-
const current = store.getSessions();
|
|
29
|
-
for (const session of current) {
|
|
30
|
-
const paneId = session.legacyPaneId ?? session.tmux?.pane;
|
|
31
|
-
if (!isClaudeManagedSession(session) || !paneId) continue;
|
|
32
|
-
if (!livePaneIds.has(paneId)) {
|
|
33
|
-
store.clearTmuxBindingForPane(paneId);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function clearStaleCodexTmuxBindings(store: DaemonStore, livePaneIds: ReadonlySet<string>): void {
|
|
39
|
-
const current = store.getSessions();
|
|
40
|
-
for (const session of current) {
|
|
41
|
-
const paneId = session.legacyPaneId ?? session.tmux?.pane;
|
|
42
|
-
if (!isCodexManagedSession(session) || !paneId) continue;
|
|
43
|
-
if (!livePaneIds.has(paneId)) {
|
|
44
|
-
store.clearTmuxBindingForPane(paneId);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Compute stableForMs for a managed session (Claude/OpenCode) being enriched
|
|
51
|
-
* with tmux context. The pane classifier's stableForMs tracks how long the
|
|
52
|
-
* *pane* has been in the same heuristic state, which is unrelated to when the
|
|
53
|
-
* managed session's state last changed (that's driven by hooks/SDK).
|
|
54
|
-
* Using the pane value would make the timer jump to minutes the moment tmux
|
|
55
|
-
* polls, even if the session state just changed moments ago.
|
|
56
|
-
*/
|
|
57
|
-
export function managedStableForMs(session: SessionModel): number {
|
|
58
|
-
const since = session.stateChangedAt ?? session.lastActivity;
|
|
59
|
-
if (!since) return 0;
|
|
60
|
-
return Math.max(0, Date.now() - since);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function detectClaudePermissionPrompt(sample: PaneSample | null): { state: SessionModel["state"]; hasAttention: boolean; summary: string } | null {
|
|
64
|
-
if (!sample) return null;
|
|
65
|
-
const text = sample.lines.join("\n");
|
|
66
|
-
if (!/Do you want to proceed\?/i.test(text)) return null;
|
|
67
|
-
return {
|
|
68
|
-
state: "waiting-input",
|
|
69
|
-
hasAttention: true,
|
|
70
|
-
summary: "Do you want to proceed?",
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export function isClaudePaneCandidate(
|
|
75
|
-
processTree: Array<{ command: string; comm: string }>,
|
|
76
|
-
currentCommand: string,
|
|
77
|
-
): boolean {
|
|
78
|
-
const combined = `${currentCommand} ${processTree.map((p) => `${p.comm} ${p.command}`).join(" ")}`;
|
|
79
|
-
// Match 'claude' binary but not 'claude-code' or other unrelated uses
|
|
80
|
-
return /(?:^|\s|\/)(claude)(?:\s|$)/i.test(combined);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export function isCodexPaneCandidate(
|
|
84
|
-
processTree: Array<{ command: string; comm: string }>,
|
|
85
|
-
currentCommand: string,
|
|
86
|
-
): boolean {
|
|
87
|
-
const combined = `${currentCommand} ${processTree.map((p) => `${p.comm} ${p.command}`).join(" ")}`;
|
|
88
|
-
return /(?:^|\s|\/)(codex)(?:\s|$)/i.test(combined);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function detectCodexPermissionPrompt(sample: PaneSample | null): { state: SessionModel["state"]; hasAttention: boolean; summary: string } | null {
|
|
92
|
-
if (!sample) return null;
|
|
93
|
-
const text = sample.lines.join("\n");
|
|
94
|
-
|
|
95
|
-
// Codex permission prompts for tool execution approval
|
|
96
|
-
const permMatch = text.match(/approve(?:d)?\s+(?:codex\s+)?to\s+run\s+(\S+)/i)
|
|
97
|
-
?? text.match(/Allow\s+(?:this\s+)?(?:command|action|tool)\??/i)
|
|
98
|
-
?? text.match(/\[Y\]es\/\[N\]o/i);
|
|
99
|
-
if (permMatch) {
|
|
100
|
-
return {
|
|
101
|
-
state: "waiting-input",
|
|
102
|
-
hasAttention: true,
|
|
103
|
-
summary: "Permission request",
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Codex agent asking a question (bullet-point lines ending in ?)
|
|
108
|
-
const questionLines = sample.lines.filter((line) => /^\s*•\s.*\?\s*$/.test(line));
|
|
109
|
-
if (questionLines.length > 0) {
|
|
110
|
-
const last = questionLines[questionLines.length - 1]!.replace(/^\s*•\s*/, "").trim();
|
|
111
|
-
return {
|
|
112
|
-
state: "waiting-input",
|
|
113
|
-
hasAttention: true,
|
|
114
|
-
summary: last.slice(0, 120) || "Waiting for input",
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const nonEmpty = sample.lines.filter((l) => l.trim().length > 0);
|
|
119
|
-
const lastNonEmpty = nonEmpty[nonEmpty.length - 1];
|
|
120
|
-
|
|
121
|
-
// Prompt cursor, optionally with a prefilled confirmation question.
|
|
122
|
-
const promptMatch = lastNonEmpty?.match(/^\s*›\s*(.*)$/);
|
|
123
|
-
if (promptMatch) {
|
|
124
|
-
const promptText = (promptMatch[1] ?? "").trim();
|
|
125
|
-
if (promptText.length > 0 && /\?\s*$/.test(promptText)) {
|
|
126
|
-
return {
|
|
127
|
-
state: "waiting-input",
|
|
128
|
-
hasAttention: true,
|
|
129
|
-
summary: promptText.slice(0, 120),
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
export function clearStaleOpenCodeTmuxBindings(store: DaemonStore, livePaneIds: ReadonlySet<string>): void {
|
|
138
|
-
const current = store.getSessions();
|
|
139
|
-
for (const session of current) {
|
|
140
|
-
const paneId = session.legacyPaneId ?? session.tmux?.pane;
|
|
141
|
-
if (!isOpenCodeManagedSession(session) || !paneId) {
|
|
142
|
-
continue;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (!livePaneIds.has(paneId)) {
|
|
146
|
-
store.clearTmuxBindingForPane(paneId);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export function isOpenCodePaneCandidate(
|
|
152
|
-
pane: TmuxPaneRecord,
|
|
153
|
-
processTree: Array<{ command: string }>,
|
|
154
|
-
classifiedTool?: string,
|
|
155
|
-
): boolean {
|
|
156
|
-
return Boolean(
|
|
157
|
-
pane.opencodeSession ||
|
|
158
|
-
classifiedTool === "opencode" ||
|
|
159
|
-
processTree.some((process) => parseOpencodeUrl(process.command) !== undefined) ||
|
|
160
|
-
isAttachClientPane(processTree),
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function isAttachClientPane(processTree: Array<{ command: string }>): boolean {
|
|
165
|
-
return processTree.some(
|
|
166
|
-
(process) =>
|
|
167
|
-
/(?:^|\s)(?:opencode|node\s+.*\/opencode|bun\s+.*\/opencode)\s+attach\b/i.test(process.command) ||
|
|
168
|
-
/\bbun\s+run\s+src\/oc\/index\.ts\b/i.test(process.command) ||
|
|
169
|
-
/(?:^|\s)(?:oc|[^\s]*\/oc)(?:\s|$)/i.test(process.command),
|
|
170
|
-
);
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
export function shouldHideOpenCodePane(
|
|
174
|
-
opencode: OpenCodeConfig,
|
|
175
|
-
pane: TmuxPaneRecord,
|
|
176
|
-
processTree: Array<{ command: string }>,
|
|
177
|
-
classifiedTool?: string,
|
|
178
|
-
): boolean {
|
|
179
|
-
if (opencode.mode !== "sse") {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return isOpenCodePaneCandidate(pane, processTree, classifiedTool);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export function looksLikeAgent(processTree: Array<{ command: string; comm: string }>, currentCommand: string): boolean {
|
|
187
|
-
const combined = `${currentCommand} ${processTree.map((p) => `${p.comm} ${p.command}`).join(" ")}`;
|
|
188
|
-
return /(opencode|codex|copilotcli|copilot|claude)/i.test(combined);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Converts a classified pane record into a SessionModel.
|
|
193
|
-
* Panes that are already covered by SDK collectors (opencode/claude) will
|
|
194
|
-
* be marked with legacyPaneId so those collectors can take precedence.
|
|
195
|
-
*/
|
|
196
|
-
export function paneModelToSession(pane: ReturnType<typeof classifyPane>["pane"]): SessionModel {
|
|
197
|
-
const tool = pane.tool === "opencode" ? "opencode"
|
|
198
|
-
: pane.tool === "claude" ? "claude"
|
|
199
|
-
: pane.tool === "codex" ? "codex"
|
|
200
|
-
: pane.tool === "copilotcli" ? "copilotcli"
|
|
201
|
-
: pane.tool === "shell" ? "shell"
|
|
202
|
-
: "unknown";
|
|
203
|
-
|
|
204
|
-
const state = pane.state === "working" ? "working"
|
|
205
|
-
: pane.state === "idle" ? "idle"
|
|
206
|
-
: pane.state === "waiting-input" ? "waiting-input"
|
|
207
|
-
: pane.state === "shell" ? "shell"
|
|
208
|
-
: "unknown";
|
|
209
|
-
const claudePrompt = tool === "claude" ? detectClaudePermissionPrompt(pane.sample) : null;
|
|
210
|
-
const codexPrompt = tool === "codex" ? detectCodexPermissionPrompt(pane.sample) : null;
|
|
211
|
-
|
|
212
|
-
return {
|
|
213
|
-
id: `tmux:${pane.paneId}`,
|
|
214
|
-
source: "tmux",
|
|
215
|
-
tool,
|
|
216
|
-
state: codexPrompt?.state ?? claudePrompt?.state ?? state,
|
|
217
|
-
project: pane.project,
|
|
218
|
-
tmux: {
|
|
219
|
-
session: pane.sessionName,
|
|
220
|
-
window: String(pane.windowIndex),
|
|
221
|
-
windowName: pane.windowName,
|
|
222
|
-
pane: pane.paneId,
|
|
223
|
-
},
|
|
224
|
-
summary: codexPrompt?.summary ?? claudePrompt?.summary ?? pane.summary,
|
|
225
|
-
lastActivity: pane.lastChangedAt,
|
|
226
|
-
detectionSource: "fallback-detection",
|
|
227
|
-
legacyPaneId: pane.paneId,
|
|
228
|
-
// Extended fields
|
|
229
|
-
processTree: pane.processTree,
|
|
230
|
-
processChain: pane.processChain,
|
|
231
|
-
sample: pane.sample,
|
|
232
|
-
toolReasons: pane.toolReasons,
|
|
233
|
-
stateReasons: pane.stateReasons,
|
|
234
|
-
stableForMs: pane.stableForMs,
|
|
235
|
-
hasAttention: codexPrompt?.hasAttention ?? claudePrompt?.hasAttention ?? pane.hasAttention,
|
|
236
|
-
currentCommand: pane.currentCommand,
|
|
237
|
-
currentPath: pane.currentPath,
|
|
238
|
-
panePid: pane.panePid,
|
|
239
|
-
tty: pane.tty,
|
|
240
|
-
paneTitle: pane.title,
|
|
241
|
-
};
|
|
242
|
-
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export { TmuxCollector } from "./collector";
|
|
2
|
-
export { clearStaleClaudeTmuxBindings, clearStaleCodexTmuxBindings, clearStaleOpenCodeTmuxBindings, isAttachClientPane, shouldHideOpenCodePane, looksLikeAgent } from "./enrichment";
|
|
3
|
-
export { collectTmuxState, parseSessions, parsePanes, capturePane } from "./parse";
|
|
4
|
-
export type { TmuxCollectionResult } from "./parse";
|
|
@@ -1,128 +0,0 @@
|
|
|
1
|
-
import type { PaneSample, SessionInfo, TmuxPaneRecord } from "../../../shared/types";
|
|
2
|
-
import { compactWhitespace, maybeRunCommand, parseIntSafe, pickMeaningfulTail, stripAnsiAndUi } from "../../../shared/utils";
|
|
3
|
-
|
|
4
|
-
export const PANE_FORMAT = [
|
|
5
|
-
"#{session_name}",
|
|
6
|
-
"#{window_index}",
|
|
7
|
-
"#{window_name}",
|
|
8
|
-
"#{pane_index}",
|
|
9
|
-
"#{pane_id}",
|
|
10
|
-
"#{pane_pid}",
|
|
11
|
-
"#{pane_current_command}",
|
|
12
|
-
"#{pane_current_path}",
|
|
13
|
-
"#{pane_active}",
|
|
14
|
-
"#{pane_tty}",
|
|
15
|
-
"#{pane_title}",
|
|
16
|
-
"#{pane_width}",
|
|
17
|
-
"#{pane_height}",
|
|
18
|
-
"#{@opencode-session}",
|
|
19
|
-
"#{@claude-session}",
|
|
20
|
-
"#{@codex-session}",
|
|
21
|
-
].join("\t");
|
|
22
|
-
|
|
23
|
-
export const SESSION_FORMAT = ["#{session_name}", "#{session_attached}", "#{session_windows}"].join("\t");
|
|
24
|
-
|
|
25
|
-
export interface TmuxCollectionResult {
|
|
26
|
-
sessions: SessionInfo[];
|
|
27
|
-
panes: TmuxPaneRecord[];
|
|
28
|
-
errors: string[];
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function collectTmuxState(timeoutMs?: number): TmuxCollectionResult {
|
|
32
|
-
const sessionResult = maybeRunCommand(["tmux", "list-sessions", "-F", SESSION_FORMAT], timeoutMs);
|
|
33
|
-
if (!sessionResult.ok) {
|
|
34
|
-
if (
|
|
35
|
-
sessionResult.error.includes("failed to connect to server") ||
|
|
36
|
-
sessionResult.error.includes("no server running")
|
|
37
|
-
) {
|
|
38
|
-
return { sessions: [], panes: [], errors: [] };
|
|
39
|
-
}
|
|
40
|
-
return { sessions: [], panes: [], errors: [sessionResult.error] };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const paneResult = maybeRunCommand(["tmux", "list-panes", "-a", "-F", PANE_FORMAT], timeoutMs);
|
|
44
|
-
const sessions = parseSessions(sessionResult.output);
|
|
45
|
-
|
|
46
|
-
if (!paneResult.ok) {
|
|
47
|
-
return { sessions, panes: [], errors: [paneResult.error] };
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return { sessions, panes: parsePanes(paneResult.output), errors: [] };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function capturePane(paneId: string, lines: number, timeoutMs?: number): PaneSample | null {
|
|
54
|
-
const result = maybeRunCommand(["tmux", "capture-pane", "-p", "-t", paneId, "-S", `-${lines}`], timeoutMs);
|
|
55
|
-
if (!result.ok) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const allLines = result.output.split(/\r?\n/).map((line: string) => line.replace(/\t/g, " "));
|
|
60
|
-
const cleanLines = allLines.filter(
|
|
61
|
-
(line: string, index: number, arr: string[]) => !(index === arr.length - 1 && line === ""),
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
return {
|
|
65
|
-
paneId,
|
|
66
|
-
lines: cleanLines,
|
|
67
|
-
preview: pickMeaningfulTail(cleanLines, compactWhitespace(stripAnsiAndUi(cleanLines.slice(-3).join(" ")))),
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function parseSessions(output: string): SessionInfo[] {
|
|
72
|
-
return output
|
|
73
|
-
.split(/\r?\n/)
|
|
74
|
-
.filter(Boolean)
|
|
75
|
-
.map((line) => {
|
|
76
|
-
const [name, attached, windows] = line.split("\t");
|
|
77
|
-
return {
|
|
78
|
-
name: name ?? "",
|
|
79
|
-
attached: attached === "1",
|
|
80
|
-
windows: parseIntSafe(windows ?? ""),
|
|
81
|
-
};
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
export function parsePanes(output: string): TmuxPaneRecord[] {
|
|
86
|
-
return output
|
|
87
|
-
.split(/\r?\n/)
|
|
88
|
-
.filter(Boolean)
|
|
89
|
-
.map((line) => {
|
|
90
|
-
const [
|
|
91
|
-
sessionName,
|
|
92
|
-
windowIndex,
|
|
93
|
-
windowName,
|
|
94
|
-
paneIndex,
|
|
95
|
-
paneId,
|
|
96
|
-
panePid,
|
|
97
|
-
currentCommand,
|
|
98
|
-
currentPath,
|
|
99
|
-
active,
|
|
100
|
-
tty,
|
|
101
|
-
title,
|
|
102
|
-
width,
|
|
103
|
-
height,
|
|
104
|
-
opencodeSession,
|
|
105
|
-
claudeSession,
|
|
106
|
-
codexSession,
|
|
107
|
-
] = line.split("\t");
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
sessionName: sessionName ?? "",
|
|
111
|
-
windowIndex: parseIntSafe(windowIndex ?? ""),
|
|
112
|
-
windowName: windowName ?? "",
|
|
113
|
-
paneIndex: parseIntSafe(paneIndex ?? ""),
|
|
114
|
-
paneId: paneId ?? "",
|
|
115
|
-
panePid: parseIntSafe(panePid ?? ""),
|
|
116
|
-
currentCommand: currentCommand ?? "",
|
|
117
|
-
currentPath: currentPath ?? "",
|
|
118
|
-
active: active === "1",
|
|
119
|
-
tty: tty ?? "",
|
|
120
|
-
title: title ?? "",
|
|
121
|
-
width: parseIntSafe(width ?? ""),
|
|
122
|
-
height: parseIntSafe(height ?? ""),
|
|
123
|
-
opencodeSession: opencodeSession || undefined,
|
|
124
|
-
claudeSession: claudeSession || undefined,
|
|
125
|
-
codexSession: codexSession || undefined,
|
|
126
|
-
} satisfies TmuxPaneRecord;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import type { ScrylogConfig } from "../../shared/config";
|
|
2
|
-
import type { Logger } from "../../shared/logger";
|
|
3
|
-
import type { UsageState, UsageProviderEntry } from "../../usage/types";
|
|
4
|
-
import { ClaudeSourcePlanner } from "../../usage/planner";
|
|
5
|
-
import { ClaudeUsageProvider } from "../../usage/providers/claude";
|
|
6
|
-
import { CodexUsageProvider } from "../../usage/providers/codex";
|
|
7
|
-
import { OpenCodeGoUsageProvider } from "../../usage/providers/opencode-go";
|
|
8
|
-
import { ClaudeAuthResolver } from "../../usage/resolvers/claude-auth";
|
|
9
|
-
import { ClaudeWebResolver } from "../../usage/resolvers/claude-web";
|
|
10
|
-
import { ClaudeCliResolver } from "../../usage/resolvers/claude-cli";
|
|
11
|
-
import { CodexAuthResolver } from "../../usage/resolvers/codex-auth";
|
|
12
|
-
import { OpenCodeGoWebResolver } from "../../usage/resolvers/opencode-go-web";
|
|
13
|
-
import { keychainCoordinator } from "../../usage/resolvers/keychain-coordinator";
|
|
14
|
-
import type { DaemonStore } from "../store";
|
|
15
|
-
import type { Collector } from "../scheduler";
|
|
16
|
-
|
|
17
|
-
interface UsageResolverSet {
|
|
18
|
-
claudeProvider: ClaudeUsageProvider;
|
|
19
|
-
codexProvider: CodexUsageProvider;
|
|
20
|
-
opencodeGoProvider: OpenCodeGoUsageProvider;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export class UsageCollector implements Collector {
|
|
24
|
-
name = "usage";
|
|
25
|
-
private timer?: ReturnType<typeof setInterval>;
|
|
26
|
-
private store?: DaemonStore;
|
|
27
|
-
private config?: ScrylogConfig;
|
|
28
|
-
private logger?: Logger;
|
|
29
|
-
private polling = false;
|
|
30
|
-
private skippedPolls = 0;
|
|
31
|
-
private errorCount = 0;
|
|
32
|
-
private pollController?: AbortController;
|
|
33
|
-
private resolverSet?: UsageResolverSet;
|
|
34
|
-
|
|
35
|
-
async start(store: DaemonStore, config: ScrylogConfig, logger: Logger): Promise<void> {
|
|
36
|
-
this.store = store;
|
|
37
|
-
this.config = config;
|
|
38
|
-
this.logger = logger;
|
|
39
|
-
keychainCoordinator.configure({
|
|
40
|
-
allowPrompts: () => config.usage.auth.allowKeychainPrompts,
|
|
41
|
-
onFailure: (failure) =>
|
|
42
|
-
logger.warn("usage", "Keychain read failed", {
|
|
43
|
-
event: "keychain_read_failed",
|
|
44
|
-
code: failure.code,
|
|
45
|
-
service: failure.service,
|
|
46
|
-
account: failure.account ?? null,
|
|
47
|
-
cooldownUntil: failure.cooldownUntil ? new Date(failure.cooldownUntil).toISOString() : null,
|
|
48
|
-
}),
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
logger.info("usage", "Collector started", { event: "collector.started", collector: this.name, intervalMs: config.usage.pollIntervalMs, providers: config.usage.providers });
|
|
52
|
-
|
|
53
|
-
await this.safePoll();
|
|
54
|
-
|
|
55
|
-
this.timer = setInterval(() => void this.safePoll(), config.usage.pollIntervalMs);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async stop(): Promise<void> {
|
|
59
|
-
if (this.timer) {
|
|
60
|
-
clearInterval(this.timer);
|
|
61
|
-
this.timer = undefined;
|
|
62
|
-
}
|
|
63
|
-
this.pollController?.abort();
|
|
64
|
-
// Drop resolver/provider instances so a later start() rebuilds them from
|
|
65
|
-
// the current configuration instead of reusing stale constructor args.
|
|
66
|
-
this.resolverSet = undefined;
|
|
67
|
-
this.logger?.info("usage", "Collector stopped", { event: "collector.stopped", collector: this.name });
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
private async safePoll(): Promise<void> {
|
|
71
|
-
const store = this.store!;
|
|
72
|
-
if (this.polling) {
|
|
73
|
-
this.skippedPolls += 1;
|
|
74
|
-
store.setCollectorStatus(this.name, { skippedPolls: this.skippedPolls });
|
|
75
|
-
this.logger?.debug("usage", "Skipping overlapping poll", { event: "collector.poll_skipped", collector: this.name, skippedPolls: this.skippedPolls });
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
this.polling = true;
|
|
80
|
-
const pollController = new AbortController();
|
|
81
|
-
this.pollController = pollController;
|
|
82
|
-
const startedAt = Date.now();
|
|
83
|
-
try {
|
|
84
|
-
await this.poll(pollController.signal);
|
|
85
|
-
store.setCollectorStatus(this.name, { lastDurationMs: Date.now() - startedAt, skippedPolls: this.skippedPolls, errorCount: this.errorCount });
|
|
86
|
-
} catch (error) {
|
|
87
|
-
if (!pollController.signal.aborted) {
|
|
88
|
-
this.errorCount += 1;
|
|
89
|
-
this.logger?.warn("usage", "Poll failed", {
|
|
90
|
-
event: "collector.poll_failed",
|
|
91
|
-
collector: this.name,
|
|
92
|
-
errorMessage: error instanceof Error ? error.message : String(error),
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
} finally {
|
|
96
|
-
if (this.pollController === pollController) this.pollController = undefined;
|
|
97
|
-
this.polling = false;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
private async poll(signal: AbortSignal): Promise<void> {
|
|
102
|
-
const store = this.store!;
|
|
103
|
-
const config = this.config!;
|
|
104
|
-
const logger = this.logger!;
|
|
105
|
-
|
|
106
|
-
const resolvers = this.ensureResolvers(config);
|
|
107
|
-
const providers: UsageProviderEntry[] = [];
|
|
108
|
-
const attemptedAt = Date.now();
|
|
109
|
-
|
|
110
|
-
for (const providerName of config.usage.providers) {
|
|
111
|
-
if (providerName === "claude") {
|
|
112
|
-
try {
|
|
113
|
-
const result = await withAbort((requestSignal) => resolvers.claudeProvider.fetch({ signal: requestSignal }), config.daemon.networkTimeoutMs, "Claude usage fetch timed out", signal);
|
|
114
|
-
providers.push(result.entry);
|
|
115
|
-
} catch (err) {
|
|
116
|
-
if (signal.aborted) throw err;
|
|
117
|
-
this.errorCount += 1;
|
|
118
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
119
|
-
logger.warn("usage", "Claude provider unexpected error", { event: "provider.fetch_failed", provider: "claude", errorMessage: message });
|
|
120
|
-
providers.push({
|
|
121
|
-
provider: "claude",
|
|
122
|
-
accountId: null,
|
|
123
|
-
available: true,
|
|
124
|
-
status: "error",
|
|
125
|
-
errors: [message],
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
} else if (providerName === "codex") {
|
|
129
|
-
try {
|
|
130
|
-
const result = await withAbort((requestSignal) => resolvers.codexProvider.fetch({ signal: requestSignal }), config.daemon.networkTimeoutMs, "Codex usage fetch timed out", signal);
|
|
131
|
-
providers.push(result.entry);
|
|
132
|
-
} catch (err) {
|
|
133
|
-
if (signal.aborted) throw err;
|
|
134
|
-
this.errorCount += 1;
|
|
135
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
136
|
-
logger.warn("usage", "Codex provider unexpected error", { event: "provider.fetch_failed", provider: "codex", errorMessage: message });
|
|
137
|
-
providers.push({
|
|
138
|
-
provider: "codex",
|
|
139
|
-
accountId: null,
|
|
140
|
-
available: true,
|
|
141
|
-
status: "error",
|
|
142
|
-
errors: [message],
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
} else if (providerName === "opencode-go") {
|
|
146
|
-
try {
|
|
147
|
-
const result = await withAbort((requestSignal) => resolvers.opencodeGoProvider.fetch({ signal: requestSignal }), config.daemon.networkTimeoutMs, "OpenCode Go usage fetch timed out", signal);
|
|
148
|
-
providers.push(result.entry);
|
|
149
|
-
} catch (err) {
|
|
150
|
-
if (signal.aborted) throw err;
|
|
151
|
-
this.errorCount += 1;
|
|
152
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
153
|
-
logger.warn("usage", "OpenCode Go provider unexpected error", { event: "provider.fetch_failed", provider: "opencode-go", errorMessage: message });
|
|
154
|
-
providers.push({
|
|
155
|
-
provider: "opencode-go",
|
|
156
|
-
accountId: null,
|
|
157
|
-
available: true,
|
|
158
|
-
status: "error",
|
|
159
|
-
errors: [message],
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const state: UsageState = {
|
|
166
|
-
providers,
|
|
167
|
-
lastRefreshedAt: attemptedAt,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
store.setUsage(state);
|
|
171
|
-
store.setCollectorStatus(this.name, { running: true, lastRunAt: attemptedAt, skippedPolls: this.skippedPolls, errorCount: this.errorCount });
|
|
172
|
-
logger.debug("usage", "Poll complete", { event: "collector.poll_completed", collector: this.name, providerCount: providers.length });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Resolvers and providers are created once per collector lifetime so
|
|
177
|
-
* resolved credentials stay cached across polls instead of re-reading
|
|
178
|
-
* credential sources on every cycle.
|
|
179
|
-
*/
|
|
180
|
-
private ensureResolvers(config: ScrylogConfig): UsageResolverSet {
|
|
181
|
-
if (!this.resolverSet) {
|
|
182
|
-
const oauthResolver = new ClaudeAuthResolver();
|
|
183
|
-
const webResolver = new ClaudeWebResolver();
|
|
184
|
-
const cliResolver = new ClaudeCliResolver();
|
|
185
|
-
const planner = new ClaudeSourcePlanner(
|
|
186
|
-
{
|
|
187
|
-
dataSource: config.usage.claude.dataSource,
|
|
188
|
-
webExtrasEnabled: config.usage.claude.webExtrasEnabled,
|
|
189
|
-
},
|
|
190
|
-
oauthResolver,
|
|
191
|
-
webResolver,
|
|
192
|
-
cliResolver,
|
|
193
|
-
);
|
|
194
|
-
|
|
195
|
-
this.resolverSet = {
|
|
196
|
-
claudeProvider: new ClaudeUsageProvider(planner, oauthResolver, webResolver, cliResolver),
|
|
197
|
-
codexProvider: new CodexUsageProvider(
|
|
198
|
-
new CodexAuthResolver(config.usage.codex.chatgptBaseUrl),
|
|
199
|
-
config.usage.codex.dataSource,
|
|
200
|
-
),
|
|
201
|
-
opencodeGoProvider: new OpenCodeGoUsageProvider(
|
|
202
|
-
new OpenCodeGoWebResolver(),
|
|
203
|
-
config.usage.opencodeGo.serverFnId,
|
|
204
|
-
),
|
|
205
|
-
};
|
|
206
|
-
}
|
|
207
|
-
return this.resolverSet;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export function withAbort<T>(
|
|
212
|
-
run: (signal: AbortSignal) => Promise<T>,
|
|
213
|
-
timeoutMs: number,
|
|
214
|
-
message: string,
|
|
215
|
-
parentSignal?: AbortSignal,
|
|
216
|
-
): Promise<T> {
|
|
217
|
-
const controller = new AbortController();
|
|
218
|
-
if (parentSignal?.aborted) {
|
|
219
|
-
controller.abort();
|
|
220
|
-
return Promise.reject(new Error("Usage poll cancelled"));
|
|
221
|
-
}
|
|
222
|
-
return new Promise<T>((resolve, reject) => {
|
|
223
|
-
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
224
|
-
const cleanup = () => {
|
|
225
|
-
if (timer) clearTimeout(timer);
|
|
226
|
-
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
227
|
-
};
|
|
228
|
-
const onParentAbort = () => {
|
|
229
|
-
controller.abort();
|
|
230
|
-
reject(new Error("Usage poll cancelled"));
|
|
231
|
-
};
|
|
232
|
-
parentSignal?.addEventListener("abort", onParentAbort, { once: true });
|
|
233
|
-
// Register the deadline before starting the request so the first
|
|
234
|
-
// synchronous step is already bounded.
|
|
235
|
-
timer = setTimeout(() => {
|
|
236
|
-
controller.abort();
|
|
237
|
-
reject(new Error(message));
|
|
238
|
-
}, timeoutMs);
|
|
239
|
-
|
|
240
|
-
let promise: Promise<T>;
|
|
241
|
-
try {
|
|
242
|
-
promise = run(controller.signal);
|
|
243
|
-
} catch (error) {
|
|
244
|
-
cleanup();
|
|
245
|
-
reject(error instanceof Error ? error : new Error(String(error)));
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
promise.then(
|
|
249
|
-
(value) => {
|
|
250
|
-
cleanup();
|
|
251
|
-
resolve(value);
|
|
252
|
-
},
|
|
253
|
-
(error) => {
|
|
254
|
-
cleanup();
|
|
255
|
-
reject(error);
|
|
256
|
-
},
|
|
257
|
-
);
|
|
258
|
-
});
|
|
259
|
-
}
|