ocwatch 0.3.0 → 0.5.0
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 +6 -0
- package/package.json +1 -1
- package/src/client/dist/assets/index-BIu7r5_5.css +1 -0
- package/src/client/dist/assets/index-CzAaOuyw.js +50 -0
- package/src/client/dist/index.html +2 -2
- package/src/server/__tests__/helpers/testDb.ts +220 -0
- package/src/server/index.ts +20 -5
- package/src/server/logic/activityLogic.ts +260 -0
- package/src/server/logic/index.ts +2 -0
- package/src/server/logic/sessionLogic.ts +107 -0
- package/src/server/routes/parts.ts +9 -7
- package/src/server/routes/poll.ts +34 -45
- package/src/server/routes/projects.ts +4 -4
- package/src/server/routes/sessions.ts +107 -68
- package/src/server/routes/sse.ts +10 -4
- package/src/server/services/parsing.ts +211 -0
- package/src/server/services/pollService.ts +292 -114
- package/src/server/services/sessionService.ts +178 -106
- package/src/server/storage/db.ts +71 -0
- package/src/server/storage/index.ts +22 -0
- package/src/server/storage/queries.ts +325 -0
- package/src/server/utils/projectResolver.ts +2 -2
- package/src/server/utils/sessionStatus.ts +4 -70
- package/src/server/watcher.ts +187 -82
- package/src/shared/constants.ts +1 -0
- package/src/shared/types/index.ts +39 -5
- package/src/shared/utils/formatTime.ts +3 -1
- package/src/client/dist/assets/index-27vUxwIP.css +0 -1
- package/src/client/dist/assets/index-B1aj6-ff.js +0 -41
- package/src/server/storage/messageParser.ts +0 -169
- package/src/server/storage/partParser.ts +0 -532
- package/src/server/storage/sessionParser.ts +0 -180
- package/src/shared/utils/burstGrouping.ts +0 -99
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Parser - Parse OpenCode session JSON files
|
|
3
|
-
* Reads from ~/.local/share/opencode/storage/session/{projectID}/{sessionID}.json
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readdir, readFile } from "node:fs/promises";
|
|
7
|
-
import { join } from "node:path";
|
|
8
|
-
import { homedir } from "node:os";
|
|
9
|
-
import type { SessionMetadata } from "../../shared/types";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Internal JSON structure from OpenCode storage
|
|
13
|
-
*/
|
|
14
|
-
interface SessionJSON {
|
|
15
|
-
id: string;
|
|
16
|
-
slug: string;
|
|
17
|
-
version?: string;
|
|
18
|
-
projectID: string;
|
|
19
|
-
directory: string;
|
|
20
|
-
title: string;
|
|
21
|
-
parentID?: string;
|
|
22
|
-
time: {
|
|
23
|
-
created: number; // Unix timestamp in milliseconds
|
|
24
|
-
updated: number;
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function getStoragePath(): string {
|
|
29
|
-
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
30
|
-
if (xdgDataHome) {
|
|
31
|
-
return xdgDataHome;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const home = homedir();
|
|
35
|
-
return join(home, ".local", "share");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export async function checkStorageExists(): Promise<boolean> {
|
|
39
|
-
try {
|
|
40
|
-
const basePath = getStoragePath();
|
|
41
|
-
const storagePath = join(basePath, "opencode", "storage");
|
|
42
|
-
await readdir(storagePath);
|
|
43
|
-
return true;
|
|
44
|
-
} catch {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* Parse a single session JSON file
|
|
51
|
-
* @param filePath - Absolute path to session JSON file
|
|
52
|
-
* @returns SessionMetadata or null if file doesn't exist or is invalid
|
|
53
|
-
*/
|
|
54
|
-
export async function parseSession(
|
|
55
|
-
filePath: string
|
|
56
|
-
): Promise<SessionMetadata | null> {
|
|
57
|
-
try {
|
|
58
|
-
const content = await readFile(filePath, "utf-8");
|
|
59
|
-
const json: SessionJSON = JSON.parse(content);
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
id: json.id,
|
|
63
|
-
projectID: json.projectID,
|
|
64
|
-
directory: json.directory,
|
|
65
|
-
title: json.title,
|
|
66
|
-
parentID: json.parentID,
|
|
67
|
-
createdAt: new Date(json.time.created),
|
|
68
|
-
updatedAt: new Date(json.time.updated),
|
|
69
|
-
};
|
|
70
|
-
} catch (error) {
|
|
71
|
-
if (error instanceof SyntaxError) {
|
|
72
|
-
console.warn(`Corrupted JSON file: ${filePath}`);
|
|
73
|
-
}
|
|
74
|
-
return null;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Get a specific session by projectID and sessionID
|
|
80
|
-
* @param projectID - Project hash ID
|
|
81
|
-
* @param sessionID - Session ID
|
|
82
|
-
* @param storagePath - Optional custom storage path (defaults to XDG path)
|
|
83
|
-
* @returns SessionMetadata or null if not found
|
|
84
|
-
*/
|
|
85
|
-
export async function getSession(
|
|
86
|
-
projectID: string,
|
|
87
|
-
sessionID: string,
|
|
88
|
-
storagePath?: string
|
|
89
|
-
): Promise<SessionMetadata | null> {
|
|
90
|
-
const basePath = storagePath || getStoragePath();
|
|
91
|
-
const filePath = join(
|
|
92
|
-
basePath,
|
|
93
|
-
"opencode",
|
|
94
|
-
"storage",
|
|
95
|
-
"session",
|
|
96
|
-
projectID,
|
|
97
|
-
`${sessionID}.json`
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
return parseSession(filePath);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* List all sessions for a given project
|
|
105
|
-
* @param projectID - Project hash ID
|
|
106
|
-
* @param storagePath - Optional custom storage path (defaults to XDG path)
|
|
107
|
-
* @returns Array of SessionMetadata (empty array if directory doesn't exist)
|
|
108
|
-
*/
|
|
109
|
-
export async function listSessions(
|
|
110
|
-
projectID: string,
|
|
111
|
-
storagePath?: string
|
|
112
|
-
): Promise<SessionMetadata[]> {
|
|
113
|
-
const basePath = storagePath || getStoragePath();
|
|
114
|
-
const sessionDir = join(
|
|
115
|
-
basePath,
|
|
116
|
-
"opencode",
|
|
117
|
-
"storage",
|
|
118
|
-
"session",
|
|
119
|
-
projectID
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const entries = await readdir(sessionDir);
|
|
124
|
-
const results = await Promise.all(
|
|
125
|
-
entries
|
|
126
|
-
.filter((entry) => entry.endsWith(".json"))
|
|
127
|
-
.map((entry) => getSession(projectID, entry.slice(0, -5), storagePath))
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
return results.filter(
|
|
131
|
-
(session): session is SessionMetadata => session !== null
|
|
132
|
-
);
|
|
133
|
-
} catch (error) {
|
|
134
|
-
// Graceful handling: return empty array if directory doesn't exist
|
|
135
|
-
return [];
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* List all project IDs from the session storage
|
|
141
|
-
* @param storagePath - Optional custom storage path (defaults to XDG path)
|
|
142
|
-
* @returns Array of project IDs (empty array if directory doesn't exist)
|
|
143
|
-
*/
|
|
144
|
-
export async function listProjects(
|
|
145
|
-
storagePath?: string
|
|
146
|
-
): Promise<string[]> {
|
|
147
|
-
const basePath = storagePath || getStoragePath();
|
|
148
|
-
const sessionBaseDir = join(basePath, "opencode", "storage", "session");
|
|
149
|
-
|
|
150
|
-
try {
|
|
151
|
-
const entries = await readdir(sessionBaseDir, { withFileTypes: true });
|
|
152
|
-
const projectIDs: string[] = [];
|
|
153
|
-
|
|
154
|
-
for (const entry of entries) {
|
|
155
|
-
if (entry.isDirectory()) {
|
|
156
|
-
projectIDs.push(entry.name);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
return projectIDs;
|
|
161
|
-
} catch (error) {
|
|
162
|
-
return [];
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/**
|
|
167
|
-
* List all sessions across all projects
|
|
168
|
-
* @param storagePath - Optional custom storage path (defaults to XDG path)
|
|
169
|
-
* @returns Array of SessionMetadata (empty array if directory doesn't exist)
|
|
170
|
-
*/
|
|
171
|
-
export async function listAllSessions(
|
|
172
|
-
storagePath?: string
|
|
173
|
-
): Promise<SessionMetadata[]> {
|
|
174
|
-
const projectIDs = await listProjects(storagePath);
|
|
175
|
-
const allResults = await Promise.all(
|
|
176
|
-
projectIDs.map((projectID) => listSessions(projectID, storagePath))
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
return allResults.flat();
|
|
180
|
-
}
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ActivityItem,
|
|
3
|
-
AgentSpawnActivity,
|
|
4
|
-
AgentCompleteActivity,
|
|
5
|
-
BurstEntry,
|
|
6
|
-
MilestoneEntry,
|
|
7
|
-
StreamEntry,
|
|
8
|
-
ToolCallActivity,
|
|
9
|
-
} from "../types";
|
|
10
|
-
|
|
11
|
-
function isMilestone(
|
|
12
|
-
item: ActivityItem
|
|
13
|
-
): item is AgentSpawnActivity | AgentCompleteActivity {
|
|
14
|
-
return item.type === "agent-spawn" || item.type === "agent-complete";
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function createBurstEntry(items: ToolCallActivity[]): BurstEntry {
|
|
18
|
-
const first = items[0]!;
|
|
19
|
-
const last = items[items.length - 1]!;
|
|
20
|
-
const toolBreakdown: Record<string, number> = {};
|
|
21
|
-
|
|
22
|
-
let pendingCount = 0;
|
|
23
|
-
let errorCount = 0;
|
|
24
|
-
|
|
25
|
-
for (const item of items) {
|
|
26
|
-
toolBreakdown[item.toolName] = (toolBreakdown[item.toolName] ?? 0) + 1;
|
|
27
|
-
|
|
28
|
-
if (item.state === "pending") {
|
|
29
|
-
pendingCount += 1;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
if (item.state === "error") {
|
|
33
|
-
errorCount += 1;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return {
|
|
38
|
-
id: first.id,
|
|
39
|
-
type: "burst",
|
|
40
|
-
agentName: first.agentName,
|
|
41
|
-
items,
|
|
42
|
-
toolBreakdown,
|
|
43
|
-
durationMs: new Date(last.timestamp).getTime() - new Date(first.timestamp).getTime(),
|
|
44
|
-
firstTimestamp: new Date(first.timestamp),
|
|
45
|
-
lastTimestamp: new Date(last.timestamp),
|
|
46
|
-
pendingCount,
|
|
47
|
-
errorCount,
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function groupIntoBursts(items: ActivityItem[]): StreamEntry[] {
|
|
52
|
-
const entries: StreamEntry[] = [];
|
|
53
|
-
let currentBurstItems: ToolCallActivity[] = [];
|
|
54
|
-
const chronologicalItems = [...items].sort(
|
|
55
|
-
(a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
const flushCurrentBurst = () => {
|
|
59
|
-
if (currentBurstItems.length === 0) {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
entries.push(createBurstEntry(currentBurstItems));
|
|
64
|
-
currentBurstItems = [];
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
for (const item of chronologicalItems) {
|
|
68
|
-
if (isMilestone(item)) {
|
|
69
|
-
flushCurrentBurst();
|
|
70
|
-
|
|
71
|
-
const milestone: MilestoneEntry = {
|
|
72
|
-
id: item.id,
|
|
73
|
-
type: "milestone",
|
|
74
|
-
item,
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
entries.push(milestone);
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (item.type !== "tool-call") {
|
|
82
|
-
continue;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const currentAgentName = currentBurstItems[0]?.agentName;
|
|
86
|
-
const shouldStartNewBurst =
|
|
87
|
-
currentBurstItems.length > 0 && currentAgentName !== item.agentName;
|
|
88
|
-
|
|
89
|
-
if (shouldStartNewBurst) {
|
|
90
|
-
flushCurrentBurst();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
currentBurstItems.push(item);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
flushCurrentBurst();
|
|
97
|
-
|
|
98
|
-
return entries;
|
|
99
|
-
}
|