ocwatch 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/package.json +57 -0
  4. package/src/client/dist/assets/index-BN8enf1I.css +1 -0
  5. package/src/client/dist/assets/index-qN9nCkAq.js +41 -0
  6. package/src/client/dist/index.html +14 -0
  7. package/src/client/dist/vite.svg +1 -0
  8. package/src/server/cli.ts +84 -0
  9. package/src/server/index.ts +78 -0
  10. package/src/server/middleware/error.ts +36 -0
  11. package/src/server/routes/health.ts +7 -0
  12. package/src/server/routes/index.ts +18 -0
  13. package/src/server/routes/parts.ts +19 -0
  14. package/src/server/routes/plan.ts +15 -0
  15. package/src/server/routes/poll.ts +77 -0
  16. package/src/server/routes/projects.ts +34 -0
  17. package/src/server/routes/sessions.ts +118 -0
  18. package/src/server/routes/sse.ts +91 -0
  19. package/src/server/services/pollService.ts +220 -0
  20. package/src/server/services/sessionService.ts +476 -0
  21. package/src/server/services/statsService.ts +53 -0
  22. package/src/server/storage/boulderParser.ts +113 -0
  23. package/src/server/storage/messageParser.ts +169 -0
  24. package/src/server/storage/partParser.ts +519 -0
  25. package/src/server/storage/sessionParser.ts +180 -0
  26. package/src/server/utils/sessionStatus.ts +123 -0
  27. package/src/server/validation.ts +34 -0
  28. package/src/server/watcher.ts +160 -0
  29. package/src/shared/constants.ts +22 -0
  30. package/src/shared/index.ts +2 -0
  31. package/src/shared/types/index.ts +326 -0
  32. package/src/shared/utils/RingBuffer.ts +79 -0
  33. package/src/shared/utils/activityUtils.ts +66 -0
  34. package/src/shared/utils/burstGrouping.ts +99 -0
  35. package/src/shared/utils/formatTime.ts +27 -0
@@ -0,0 +1,79 @@
1
+ /**
2
+ * RingBuffer - Generic circular buffer with fixed capacity
3
+ * Automatically drops oldest items when capacity is exceeded
4
+ *
5
+ * Used for storing recent logs and tool calls with a maximum size limit.
6
+ * Ported from Go implementation: internal/state/state.go:50-90
7
+ */
8
+
9
+ import { RINGBUFFER_CAPACITY } from "../constants";
10
+
11
+ export class RingBuffer<T> {
12
+ private buffer: T[] = [];
13
+ private capacity: number;
14
+ private head: number = 0;
15
+
16
+ /**
17
+ * Create a new RingBuffer with specified capacity
18
+ * @param capacity Maximum number of items to store (default: RINGBUFFER_CAPACITY)
19
+ */
20
+ constructor(capacity: number = RINGBUFFER_CAPACITY) {
21
+ this.capacity = Math.max(1, capacity);
22
+ }
23
+
24
+ /**
25
+ * Add an item to the buffer
26
+ * If buffer is full, drops the oldest item
27
+ * @param item The item to add
28
+ */
29
+ push(item: T): void {
30
+ if (this.buffer.length < this.capacity) {
31
+ this.buffer.push(item);
32
+ } else {
33
+ // Buffer is full, overwrite oldest item
34
+ this.buffer[this.head] = item;
35
+ this.head = (this.head + 1) % this.capacity;
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Get all items in order (oldest to newest)
41
+ * @returns Array of all items in chronological order
42
+ */
43
+ getAll(): T[] {
44
+ if (this.buffer.length < this.capacity) {
45
+ // Buffer not full yet, return as-is
46
+ return [...this.buffer];
47
+ }
48
+ // Buffer is full, reconstruct in order starting from head
49
+ return [
50
+ ...this.buffer.slice(this.head),
51
+ ...this.buffer.slice(0, this.head),
52
+ ];
53
+ }
54
+
55
+ /**
56
+ * Get the last n items (newest first)
57
+ * @param n Number of items to retrieve
58
+ * @returns Array of last n items in reverse chronological order
59
+ */
60
+ getLatest(n: number): T[] {
61
+ const all = this.getAll();
62
+ return all.slice(Math.max(0, all.length - n)).reverse();
63
+ }
64
+
65
+ /**
66
+ * Clear all items from the buffer
67
+ */
68
+ clear(): void {
69
+ this.buffer = [];
70
+ this.head = 0;
71
+ }
72
+
73
+ /**
74
+ * Get current number of items in the buffer
75
+ */
76
+ get size(): number {
77
+ return this.buffer.length;
78
+ }
79
+ }
@@ -0,0 +1,66 @@
1
+ import type { ActivitySession, ActivityItem } from '../types';
2
+
3
+ export function synthesizeActivityItems(
4
+ sessions: ActivitySession[]
5
+ ): ActivityItem[] {
6
+ const items: ActivityItem[] = [];
7
+ const sessionMap = new Map<string, ActivitySession>();
8
+ const seenToolCallIds = new Set<string>();
9
+
10
+ sessions.forEach((session) => {
11
+ sessionMap.set(session.id, session);
12
+ });
13
+
14
+ sessions.forEach((session) => {
15
+ if (session.parentID && sessionMap.has(session.parentID)) {
16
+ const parent = sessionMap.get(session.parentID)!;
17
+ items.push({
18
+ id: `spawn-${session.id}`,
19
+ type: "agent-spawn",
20
+ timestamp: session.createdAt,
21
+ agentName: parent.agent,
22
+ spawnedAgentName: session.agent,
23
+ });
24
+ }
25
+
26
+ if (session.toolCalls && session.toolCalls.length > 0) {
27
+ session.toolCalls.forEach((toolCall) => {
28
+ if (seenToolCallIds.has(toolCall.id)) return;
29
+ seenToolCallIds.add(toolCall.id);
30
+
31
+ items.push({
32
+ id: toolCall.id,
33
+ type: "tool-call",
34
+ timestamp: new Date(toolCall.timestamp),
35
+ agentName: toolCall.agentName,
36
+ toolName: toolCall.name,
37
+ state: toolCall.state,
38
+ summary: toolCall.summary,
39
+ input: toolCall.input,
40
+ });
41
+ });
42
+ }
43
+
44
+ if (session.status === "completed") {
45
+ const durationMs = session.updatedAt
46
+ ? new Date(session.updatedAt).getTime() -
47
+ new Date(session.createdAt).getTime()
48
+ : undefined;
49
+
50
+ items.push({
51
+ id: `complete-${session.id}`,
52
+ type: "agent-complete",
53
+ timestamp: session.updatedAt,
54
+ agentName: session.agent,
55
+ status: session.status,
56
+ durationMs,
57
+ });
58
+ }
59
+ });
60
+
61
+ items.sort(
62
+ (a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
63
+ );
64
+
65
+ return items;
66
+ }
@@ -0,0 +1,99 @@
1
+ import type {
2
+ ActivityItem,
3
+ BurstEntry,
4
+ MilestoneEntry,
5
+ StreamEntry,
6
+ ToolCallActivity,
7
+ } from "../types";
8
+
9
+ function isMilestone(item: ActivityItem): boolean {
10
+ if (item.type === "agent-spawn" || item.type === "agent-complete") {
11
+ return true;
12
+ }
13
+
14
+ return item.type === "tool-call" && item.state === "error";
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
+ }
@@ -0,0 +1,27 @@
1
+ /** "<1m", "3m", "2h", "3d", or locale date */
2
+ export function formatRelativeTime(date: Date | string): string {
3
+ const d = typeof date === 'string' ? new Date(date) : date;
4
+ const now = Date.now();
5
+ const diff = now - d.getTime();
6
+ const minutes = Math.floor(diff / 60000);
7
+ const hours = Math.floor(minutes / 60);
8
+
9
+ if (minutes < 1) return '<1m';
10
+ if (minutes < 60) return `${minutes}m`;
11
+ if (hours < 24) return `${hours}h`;
12
+ const days = Math.floor(hours / 24);
13
+ if (days < 7) return `${days}d`;
14
+ return d.toLocaleDateString();
15
+ }
16
+
17
+ /** "just now", "3m ago", "2h ago", "3d ago" */
18
+ export function formatRelativeTimeVerbose(date: Date | string): string {
19
+ const d = new Date(date);
20
+ const now = new Date();
21
+ const diffInSeconds = Math.max(0, Math.floor((now.getTime() - d.getTime()) / 1000));
22
+
23
+ if (diffInSeconds < 60) return 'just now';
24
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)}m ago`;
25
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)}h ago`;
26
+ return `${Math.floor(diffInSeconds / 86400)}d ago`;
27
+ }