@codewithdan/zingit 0.17.5 → 0.17.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codewithdan/zingit",
3
- "version": "0.17.5",
3
+ "version": "0.17.7",
4
4
  "description": "AI-powered UI marker tool - point, mark, and let AI fix it",
5
5
  "type": "module",
6
6
  "engines": {
@@ -60,7 +60,7 @@
60
60
  "homepage": "https://github.com/danwahlin/zingit#readme",
61
61
  "dependencies": {
62
62
  "@anthropic-ai/claude-agent-sdk": "^0.2.17",
63
- "@codewithdan/agent-sdk-core": "^0.2.0",
63
+ "@codewithdan/agent-sdk-core": "^0.3.1",
64
64
  "@github/copilot-sdk": "^0.1.16",
65
65
  "@openai/codex-sdk": ">=0.80.0",
66
66
  "diff": "^8.0.3",
@@ -271,7 +271,7 @@ export async function handleMessage(ws, state, msg, deps) {
271
271
  prompt = `[Context: User is currently on page: ${msg.pageUrl}]\n\n${msg.content}`;
272
272
  }
273
273
  // Add timeout to detect if SDK hangs
274
- const timeoutMs = 120000; // 2 minutes
274
+ const timeoutMs = 120000; // 2 minutes — client timeout (zing-ui.ts) must exceed this
275
275
  const sendPromise = state.session.send({ prompt });
276
276
  const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error('Agent response timeout')), timeoutMs));
277
277
  await Promise.race([sendPromise, timeoutPromise]);
@@ -1,7 +1,7 @@
1
1
  // server/src/index.ts
2
2
  import { WebSocketServer } from 'ws';
3
3
  import { CoreProviderAdapter } from './agents/core-adapter.js';
4
- import { CopilotProvider, ClaudeProvider, CodexProvider, } from '@codewithdan/agent-sdk-core';
4
+ import { CopilotProvider, ClaudeProvider, CodexProvider, OpenCodeProvider, } from '@codewithdan/agent-sdk-core';
5
5
  import { spawn } from 'node:child_process';
6
6
  import { userInfo } from 'node:os';
7
7
  import { detectAgents } from './utils/agent-detection.js';
@@ -57,6 +57,7 @@ function createAgentFactory() {
57
57
  copilot: () => new CoreProviderAdapter(new CopilotProvider()),
58
58
  claude: () => new CoreProviderAdapter(new ClaudeProvider(claudeOpts)),
59
59
  codex: () => new CoreProviderAdapter(new CodexProvider()),
60
+ opencode: () => new CoreProviderAdapter(new OpenCodeProvider()),
60
61
  };
61
62
  }
62
63
  const agentFactories = createAgentFactory();
@@ -12,6 +12,10 @@ export interface Checkpoint {
12
12
  filesModified: number;
13
13
  linesChanged: number;
14
14
  }
15
+ /**
16
+ * Canonical definition of MarkerSummary.
17
+ * Keep in sync with: client/src/types/index.ts (MarkerSummary)
18
+ */
15
19
  export interface MarkerSummary {
16
20
  id: string;
17
21
  identifier: string;
@@ -29,6 +33,10 @@ export interface ChangeHistory {
29
33
  checkpoints: Checkpoint[];
30
34
  currentCheckpointId: string | null;
31
35
  }
36
+ /**
37
+ * Canonical definition of CheckpointInfo.
38
+ * Keep in sync with: client/src/types/index.ts (CheckpointInfo)
39
+ */
32
40
  export interface CheckpointInfo {
33
41
  id: string;
34
42
  timestamp: string;
@@ -29,6 +29,10 @@ export interface AgentSession {
29
29
  destroy(): Promise<void>;
30
30
  getSessionId?(): string | null;
31
31
  }
32
+ /**
33
+ * Canonical definition of a Marker.
34
+ * Keep in sync with: client/src/types/index.ts (Marker)
35
+ */
32
36
  export interface Marker {
33
37
  id: string;
34
38
  selector: string;
@@ -43,6 +47,10 @@ export interface Marker {
43
47
  parentHtml?: string;
44
48
  screenshot?: string;
45
49
  }
50
+ /**
51
+ * Canonical definition of BatchData.
52
+ * Keep in sync with: client/src/types/index.ts (BatchData)
53
+ */
46
54
  export interface BatchData {
47
55
  pageUrl: string;
48
56
  pageTitle: string;
@@ -35,10 +35,11 @@ function checkCodexAuth() {
35
35
  */
36
36
  export async function detectAgents() {
37
37
  // Run all CLI checks in parallel for better performance
38
- const [claudeCheck, copilotCheck, codexCheck] = await Promise.all([
38
+ const [claudeCheck, copilotCheck, codexCheck, opencodeCheck] = await Promise.all([
39
39
  checkCLI('claude'),
40
40
  checkCLI('copilot'),
41
- checkCLI('codex')
41
+ checkCLI('codex'),
42
+ checkCLI('opencode')
42
43
  ]);
43
44
  const agents = [];
44
45
  // Claude Code
@@ -80,6 +81,15 @@ export async function detectAgents() {
80
81
  installCommand: 'npm install -g @openai/codex'
81
82
  });
82
83
  }
84
+ // OpenCode
85
+ agents.push({
86
+ name: 'opencode',
87
+ displayName: 'OpenCode',
88
+ available: opencodeCheck.installed,
89
+ version: opencodeCheck.version,
90
+ reason: opencodeCheck.installed ? undefined : 'OpenCode CLI not found',
91
+ installCommand: 'npm install -g opencode-ai'
92
+ });
83
93
  return agents;
84
94
  }
85
95
  /**
@@ -1,17 +0,0 @@
1
- import { BaseAgent } from './base.js';
2
- import type { AgentSession } from '../types.js';
3
- export declare class ClaudeCodeAgent extends BaseAgent {
4
- name: string;
5
- model: string;
6
- start(): Promise<void>;
7
- stop(): Promise<void>;
8
- /**
9
- * Build content blocks for multimodal message with images and text
10
- */
11
- private buildContentBlocks;
12
- /**
13
- * Create a generator that yields the initial user message with optional images
14
- */
15
- private createMessageGenerator;
16
- createSession(wsRef: import('../types.js').WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<AgentSession>;
17
- }
@@ -1,175 +0,0 @@
1
- // server/src/agents/claude.ts
2
- // Agent that uses Claude Agent SDK
3
- import { query } from '@anthropic-ai/claude-agent-sdk';
4
- import { BaseAgent } from './base.js';
5
- export class ClaudeCodeAgent extends BaseAgent {
6
- name = 'claude';
7
- model = 'claude-sonnet-4-20250514';
8
- async start() {
9
- console.log(`✓ Claude Agent SDK initialized (model: ${this.model})`);
10
- }
11
- async stop() {
12
- // SDK handles cleanup automatically
13
- }
14
- /**
15
- * Build content blocks for multimodal message with images and text
16
- */
17
- buildContentBlocks(prompt, images) {
18
- const content = [];
19
- // Add images first so Claude sees them before the text instructions
20
- if (images && images.length > 0) {
21
- for (const img of images) {
22
- // Add label as text before each image for context
23
- if (img.label) {
24
- content.push({ type: 'text', text: `[${img.label}]` });
25
- }
26
- content.push({
27
- type: 'image',
28
- source: {
29
- type: 'base64',
30
- media_type: img.mediaType,
31
- data: img.base64
32
- }
33
- });
34
- }
35
- }
36
- // Add the main text prompt
37
- content.push({ type: 'text', text: prompt });
38
- return content;
39
- }
40
- /**
41
- * Create a generator that yields the initial user message with optional images
42
- */
43
- async *createMessageGenerator(prompt, images) {
44
- const content = this.buildContentBlocks(prompt, images);
45
- yield {
46
- type: 'user',
47
- message: {
48
- role: 'user',
49
- content
50
- },
51
- parent_tool_use_id: null,
52
- session_id: '' // SDK will assign the actual session ID
53
- };
54
- }
55
- async createSession(wsRef, projectDir, resumeSessionId) {
56
- const send = (data) => {
57
- const ws = wsRef.current;
58
- if (ws && ws.readyState === ws.OPEN) {
59
- ws.send(JSON.stringify(data));
60
- }
61
- };
62
- // Track session ID for conversation continuity (stable V1 resume feature)
63
- // Start with provided sessionId if resuming previous conversation
64
- let sessionId = resumeSessionId;
65
- // Track sent content to avoid duplicates (in case SDK sends same message multiple times)
66
- const sentContent = new Set();
67
- return {
68
- send: async (msg) => {
69
- try {
70
- // Use generator function to pass multimodal content (text + images)
71
- const messageGenerator = this.createMessageGenerator(msg.prompt, msg.images);
72
- const response = query({
73
- prompt: messageGenerator,
74
- options: {
75
- model: this.model,
76
- cwd: projectDir,
77
- permissionMode: 'acceptEdits', // Auto-approve file edits (no interactive terminal)
78
- // Resume previous session if we have a session ID (enables follow-up conversations)
79
- ...(sessionId && { resume: sessionId }),
80
- systemPrompt: `You are a direct, efficient UI modification assistant.
81
-
82
- CRITICAL EFFICIENCY RULES:
83
- 1. Use the provided selector and HTML context to quickly locate the target element
84
- 2. Make the requested change immediately - don't explore or explain unless there's ambiguity
85
- 3. For simple changes (text, styles, attributes), be concise - just do it and confirm
86
- 4. Only search/explore if the selector doesn't match or you need to understand complex context
87
- 5. Avoid explaining what markers are or describing the codebase unnecessarily
88
-
89
- WHEN TO BE BRIEF (90% of cases):
90
- - Text changes: Find, change, confirm (1-2 sentences)
91
- - Style changes: Find, modify CSS, confirm
92
- - Simple DOM changes: Make the change, state what you did
93
-
94
- WHEN TO BE THOROUGH (10% of cases):
95
- - Ambiguous selectors (multiple matches)
96
- - Complex architectural changes
97
- - Need to understand component interactions
98
-
99
- Response format:
100
- - **Action taken:** Brief statement of what changed
101
- - **File:** Path to modified file
102
- - **Summary:** 1-2 sentence confirmation
103
-
104
- Use screenshots to verify you're targeting the right element, but don't over-explain their purpose.`
105
- }
106
- });
107
- // Process streaming response
108
- for await (const message of response) {
109
- switch (message.type) {
110
- case 'system':
111
- // Capture session ID from init message for follow-up conversations
112
- if ('subtype' in message && message.subtype === 'init') {
113
- sessionId = message.session_id;
114
- console.log('[Claude Agent] Session initialized:', sessionId);
115
- }
116
- break;
117
- case 'assistant':
118
- // Extract text content from assistant messages (content is nested in message.content)
119
- if ('message' in message && message.message && 'content' in message.message) {
120
- const content = message.message.content;
121
- if (Array.isArray(content)) {
122
- for (const block of content) {
123
- if (block.type === 'text' && block.text) {
124
- // Deduplicate content (SDK may replay conversation history)
125
- const contentHash = `${block.text.substring(0, 100)}_${block.text.length}`;
126
- if (!sentContent.has(contentHash)) {
127
- sentContent.add(contentHash);
128
- send({ type: 'delta', content: block.text });
129
- }
130
- }
131
- }
132
- }
133
- }
134
- break;
135
- case 'stream_event':
136
- // Handle streaming events for real-time updates
137
- if (message.event?.type === 'content_block_delta') {
138
- const delta = message.event.delta;
139
- if (delta && 'text' in delta) {
140
- console.log('[Claude Agent] Content block delta, text length:', delta.text.length);
141
- send({ type: 'delta', content: delta.text });
142
- }
143
- }
144
- else if (message.event?.type === 'content_block_stop') {
145
- console.log('[Claude Agent] Content block stopped');
146
- }
147
- else if (message.event?.type === 'message_stop') {
148
- console.log('[Claude Agent] Message stopped');
149
- }
150
- break;
151
- case 'tool_progress':
152
- // Tool is being executed
153
- console.log('[Claude Agent] Tool executing:', message.tool_name);
154
- send({ type: 'tool_start', tool: message.tool_name });
155
- break;
156
- case 'result':
157
- // Query completed
158
- console.log('[Claude Agent] Query completed, sending idle');
159
- send({ type: 'idle' });
160
- break;
161
- }
162
- }
163
- console.log('[Claude Agent] Response stream ended');
164
- }
165
- catch (err) {
166
- send({ type: 'error', message: err.message });
167
- }
168
- },
169
- destroy: async () => {
170
- // SDK handles session cleanup automatically
171
- },
172
- getSessionId: () => sessionId || null
173
- };
174
- }
175
- }
@@ -1,11 +0,0 @@
1
- import { BaseAgent } from './base.js';
2
- import type { AgentSession } from '../types.js';
3
- export declare class CodexAgent extends BaseAgent {
4
- name: string;
5
- model: string;
6
- private codex;
7
- constructor();
8
- start(): Promise<void>;
9
- stop(): Promise<void>;
10
- createSession(wsRef: import('../types.js').WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<AgentSession>;
11
- }
@@ -1,202 +0,0 @@
1
- // server/src/agents/codex.ts
2
- // Agent that uses OpenAI Codex SDK
3
- import { Codex } from '@openai/codex-sdk';
4
- import { BaseAgent } from './base.js';
5
- import { promises as fs } from 'fs';
6
- import * as path from 'path';
7
- import * as os from 'os';
8
- import { randomUUID } from 'crypto';
9
- export class CodexAgent extends BaseAgent {
10
- name = 'codex';
11
- model;
12
- codex = null;
13
- constructor() {
14
- super();
15
- this.model = process.env.CODEX_MODEL || 'gpt-5.2-codex';
16
- }
17
- async start() {
18
- // Initialize the Codex client
19
- // Uses cached credentials from ~/.codex/auth.json (login via `codex` CLI)
20
- this.codex = new Codex();
21
- console.log(`✓ Codex SDK initialized (model: ${this.model})`);
22
- }
23
- async stop() {
24
- // Codex SDK doesn't require explicit cleanup
25
- this.codex = null;
26
- }
27
- async createSession(wsRef, projectDir, resumeSessionId) {
28
- if (!this.codex) {
29
- throw new Error('Codex client not initialized');
30
- }
31
- const send = (data) => {
32
- const ws = wsRef.current;
33
- if (ws && ws.readyState === ws.OPEN) {
34
- ws.send(JSON.stringify(data));
35
- }
36
- };
37
- // Thread options for both new and resumed threads
38
- const threadOptions = {
39
- workingDirectory: projectDir,
40
- };
41
- // Resume existing thread if we have a thread ID, otherwise start new thread
42
- const thread = resumeSessionId
43
- ? this.codex.resumeThread(resumeSessionId, threadOptions)
44
- : this.codex.startThread(threadOptions);
45
- let abortController = null;
46
- // Track temp files for cleanup on session destroy (prevents race condition)
47
- const sessionTempFiles = [];
48
- return {
49
- send: async (msg) => {
50
- try {
51
- abortController = new AbortController();
52
- const input = [];
53
- // If images are provided, save them as temp files and add to structured input
54
- if (msg.images && msg.images.length > 0) {
55
- const tempDir = os.tmpdir();
56
- for (let i = 0; i < msg.images.length; i++) {
57
- const img = msg.images[i];
58
- // Use UUID to avoid filename collisions
59
- const ext = img.mediaType.split('/')[1] || 'png';
60
- const tempPath = path.join(tempDir, `zingit-screenshot-${randomUUID()}.${ext}`);
61
- // Decode base64 to buffer with error handling
62
- let buffer;
63
- try {
64
- buffer = Buffer.from(img.base64, 'base64');
65
- }
66
- catch (decodeErr) {
67
- console.warn(`ZingIt: Failed to decode base64 for image ${i + 1}:`, decodeErr);
68
- continue; // Skip this image
69
- }
70
- // Save with restrictive permissions (owner read/write only)
71
- await fs.writeFile(tempPath, buffer, { mode: 0o600 });
72
- sessionTempFiles.push(tempPath);
73
- // Add label text before image
74
- if (img.label) {
75
- input.push({ type: 'text', text: `[${img.label}]` });
76
- }
77
- // Add image as local_image input
78
- input.push({ type: 'local_image', path: tempPath });
79
- }
80
- }
81
- // Add system instructions and main prompt
82
- const systemInstructions = `You are a UI debugging assistant. When given markers about UI elements, search for the corresponding code using the selectors and HTML context provided, then make the requested changes.
83
-
84
- When screenshots are provided, use them to:
85
- - Better understand the visual context and styling of the elements
86
- - Identify the exact appearance that needs to be changed
87
- - Verify you're targeting the correct element based on its visual representation
88
-
89
- IMPORTANT: Format all responses using markdown:
90
- - Use **bold** for emphasis on important points
91
- - Use numbered lists for sequential steps (1. 2. 3.)
92
- - Use bullet points for non-sequential items
93
- - Use code blocks with \`\`\`language syntax for code examples
94
- - Use inline \`code\` for file paths, selectors, and technical terms
95
-
96
- `;
97
- input.push({ type: 'text', text: systemInstructions + msg.prompt });
98
- // Use runStreamed with structured input for real-time progress
99
- const { events } = await thread.runStreamed(input);
100
- for await (const event of events) {
101
- // Check if aborted
102
- if (abortController?.signal.aborted) {
103
- break;
104
- }
105
- switch (event.type) {
106
- case 'item.started':
107
- // Tool/action started
108
- if (event.item?.type) {
109
- const toolName = getToolDisplayName(event.item);
110
- send({ type: 'tool_start', tool: toolName });
111
- }
112
- break;
113
- case 'item.completed':
114
- // Item completed - extract content based on type
115
- if (event.item) {
116
- switch (event.item.type) {
117
- case 'agent_message':
118
- // Agent's text response
119
- send({ type: 'delta', content: event.item.text + '\n' });
120
- break;
121
- case 'reasoning':
122
- // Optional: show reasoning
123
- send({ type: 'delta', content: `\n*[Reasoning]* ${event.item.text}\n` });
124
- break;
125
- case 'command_execution':
126
- // Command was executed
127
- send({ type: 'delta', content: `\n$ ${event.item.command}\n${event.item.aggregated_output}\n` });
128
- break;
129
- case 'file_change':
130
- // Files were changed
131
- const files = event.item.changes.map(c => `${c.kind}: ${c.path}`).join(', ');
132
- send({ type: 'delta', content: `\n*[Files changed]* ${files}\n` });
133
- break;
134
- }
135
- send({ type: 'tool_end', tool: event.item.type });
136
- }
137
- break;
138
- case 'turn.completed':
139
- // Turn finished
140
- console.log('[Codex Agent] Turn completed, sending idle');
141
- send({ type: 'idle' });
142
- break;
143
- case 'turn.failed':
144
- // Turn failed with error
145
- send({ type: 'error', message: event.error?.message || 'Codex turn failed' });
146
- break;
147
- case 'error':
148
- send({ type: 'error', message: event.message || 'Unknown Codex error' });
149
- break;
150
- }
151
- }
152
- console.log('[Codex Agent] Event stream ended');
153
- }
154
- catch (err) {
155
- send({ type: 'error', message: err.message });
156
- }
157
- // Note: Temp files cleaned up on session destroy to avoid race condition
158
- },
159
- destroy: async () => {
160
- try {
161
- // Abort any ongoing operation
162
- if (abortController) {
163
- abortController.abort();
164
- abortController = null;
165
- }
166
- // Thread cleanup happens automatically
167
- }
168
- finally {
169
- // Clean up all temp files even if abort fails
170
- for (const tempPath of sessionTempFiles) {
171
- try {
172
- await fs.unlink(tempPath);
173
- }
174
- catch (cleanupErr) {
175
- // Ignore errors (file may already be deleted)
176
- console.warn(`ZingIt: Failed to clean up temp file ${tempPath}:`, cleanupErr.message);
177
- }
178
- }
179
- sessionTempFiles.length = 0; // Clear the array
180
- }
181
- },
182
- getSessionId: () => thread.id
183
- };
184
- }
185
- }
186
- // Helper to get a readable tool name
187
- function getToolDisplayName(item) {
188
- switch (item.type) {
189
- case 'command_execution':
190
- return `Running: ${item.command?.split(' ')[0] || 'command'}`;
191
- case 'mcp_tool_call':
192
- return `Tool: ${item.tool || 'mcp'}`;
193
- case 'file_change':
194
- return 'Editing files';
195
- case 'web_search':
196
- return 'Searching web';
197
- case 'reasoning':
198
- return 'Thinking...';
199
- default:
200
- return item.type;
201
- }
202
- }
@@ -1,11 +0,0 @@
1
- import { BaseAgent } from './base.js';
2
- import type { AgentSession } from '../types.js';
3
- export declare class CopilotAgent extends BaseAgent {
4
- name: string;
5
- model: string;
6
- private client;
7
- constructor();
8
- start(): Promise<void>;
9
- stop(): Promise<void>;
10
- createSession(wsRef: import('../types.js').WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<AgentSession>;
11
- }