@codewithdan/zingit 0.17.4 → 0.17.5

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,11 +1,15 @@
1
1
  {
2
2
  "name": "@codewithdan/zingit",
3
- "version": "0.17.4",
3
+ "version": "0.17.5",
4
4
  "description": "AI-powered UI marker tool - point, mark, and let AI fix it",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": ">=22.0.0"
8
8
  },
9
+ "workspaces": [
10
+ "client",
11
+ "server"
12
+ ],
9
13
  "bin": {
10
14
  "zingit": "bin/cli.js"
11
15
  },
@@ -21,16 +25,16 @@
21
25
  "main": "client/dist/zingit-client.js",
22
26
  "scripts": {
23
27
  "build": "npm run build:client && npm run build:server",
24
- "build:client": "cd client && npm run build",
25
- "build:server": "cd server && npm run build",
26
- "deploy": "cd client && npm run deploy",
28
+ "build:client": "npm run build -w client",
29
+ "build:server": "npm run build -w server",
30
+ "deploy": "npm run deploy -w client",
27
31
  "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
28
- "dev:server": "cd server && npm run dev",
29
- "dev:client": "cd client && npm run dev",
30
- "test": "cd client && npm run test",
31
- "test:watch": "cd client && npm run test:watch",
32
- "test:ui": "cd client && npm run test:ui",
33
- "release": "standard-version && npm run build && npm publish --access public"
32
+ "dev:server": "npm run dev -w server",
33
+ "dev:client": "npm run dev -w client",
34
+ "test": "npm run test -w client",
35
+ "test:watch": "npm run test:watch -w client",
36
+ "test:ui": "npm run test:ui -w client",
37
+ "release": "commit-and-tag-version && npm run build && npm publish --access public"
34
38
  },
35
39
  "keywords": [
36
40
  "ui",
@@ -56,22 +60,19 @@
56
60
  "homepage": "https://github.com/danwahlin/zingit#readme",
57
61
  "dependencies": {
58
62
  "@anthropic-ai/claude-agent-sdk": "^0.2.17",
63
+ "@codewithdan/agent-sdk-core": "^0.2.0",
59
64
  "@github/copilot-sdk": "^0.1.16",
60
- "@openai/codex-sdk": "^0.89.0",
65
+ "@openai/codex-sdk": ">=0.80.0",
61
66
  "diff": "^8.0.3",
62
67
  "uuid": "^11.1.0",
63
68
  "ws": "^8.19.0",
64
69
  "zod": "^4.3.6"
65
70
  },
66
71
  "devDependencies": {
67
- "@types/diff": "^6.0.0",
68
- "@types/node": "^25.0.10",
69
- "@types/uuid": "^10.0.0",
70
- "@types/ws": "^8.18.1",
71
- "concurrently": "^9.1.2",
72
- "gh-pages": "^6.3.0",
73
- "standard-version": "^9.5.0",
74
- "tsx": "^4.21.0",
75
- "typescript": "^5.9.3"
72
+ "commit-and-tag-version": "^12.6.1",
73
+ "concurrently": "^9.1.2"
74
+ },
75
+ "overrides": {
76
+ "minimatch": "^10.2.1"
76
77
  }
77
78
  }
@@ -0,0 +1,16 @@
1
+ import type { AgentSession as ZingitSession, WebSocketRef } from '../types.js';
2
+ import type { AgentProvider as CoreProvider } from '@codewithdan/agent-sdk-core';
3
+ import { BaseAgent } from './base.js';
4
+ /**
5
+ * Adapter that wraps a @codewithdan/agent-sdk-core provider to match zingit's Agent interface.
6
+ */
7
+ export declare class CoreProviderAdapter extends BaseAgent {
8
+ name: string;
9
+ model: string;
10
+ private provider;
11
+ private started;
12
+ constructor(provider: CoreProvider);
13
+ start(): Promise<void>;
14
+ stop(): Promise<void>;
15
+ createSession(wsRef: WebSocketRef, projectDir: string, resumeSessionId?: string): Promise<ZingitSession>;
16
+ }
@@ -0,0 +1,152 @@
1
+ // server/src/agents/core-adapter.ts
2
+ // Adapts @codewithdan/agent-sdk-core providers to the zingit Agent/AgentSession interface.
3
+ // This lets zingit use the shared provider implementations while keeping
4
+ // its own WS transport model (agents send directly to per-connection WebSocket).
5
+ import { BaseAgent } from './base.js';
6
+ import { promises as fs } from 'fs';
7
+ import * as path from 'path';
8
+ import * as os from 'os';
9
+ import { randomUUID } from 'crypto';
10
+ /**
11
+ * Maps core AgentEvent types to zingit WSOutgoingMessage types.
12
+ * Zingit uses a simpler event model: delta, tool_start, tool_end, idle, error.
13
+ */
14
+ function mapCoreEventToWS(event) {
15
+ switch (event.type) {
16
+ case 'output':
17
+ case 'thinking':
18
+ case 'command_output':
19
+ case 'test_result':
20
+ return { type: 'delta', content: event.content };
21
+ case 'command':
22
+ case 'file_read':
23
+ case 'file_write':
24
+ case 'file_edit':
25
+ case 'tool_call':
26
+ return { type: 'tool_start', tool: event.metadata?.command || event.content };
27
+ case 'complete':
28
+ return { type: 'idle' };
29
+ case 'error':
30
+ return { type: 'error', message: event.content };
31
+ default:
32
+ return null;
33
+ }
34
+ }
35
+ /**
36
+ * Convert zingit ImageContent[] to core AgentAttachment[].
37
+ */
38
+ function imagesToAttachments(images) {
39
+ if (!images || images.length === 0)
40
+ return undefined;
41
+ return images.map(img => ({
42
+ type: 'base64_image',
43
+ data: img.base64,
44
+ mediaType: img.mediaType,
45
+ displayName: img.label,
46
+ }));
47
+ }
48
+ /**
49
+ * Adapter that wraps a @codewithdan/agent-sdk-core provider to match zingit's Agent interface.
50
+ */
51
+ export class CoreProviderAdapter extends BaseAgent {
52
+ name;
53
+ model;
54
+ provider;
55
+ started = false;
56
+ constructor(provider) {
57
+ super();
58
+ this.name = provider.name;
59
+ this.model = provider.model;
60
+ this.provider = provider;
61
+ }
62
+ async start() {
63
+ if (!this.started) {
64
+ await this.provider.start();
65
+ this.started = true;
66
+ }
67
+ }
68
+ async stop() {
69
+ if (this.started) {
70
+ await this.provider.stop();
71
+ this.started = false;
72
+ }
73
+ }
74
+ async createSession(wsRef, projectDir, resumeSessionId) {
75
+ const send = (data) => {
76
+ const ws = wsRef.current;
77
+ if (ws && ws.readyState === ws.OPEN) {
78
+ ws.send(JSON.stringify(data));
79
+ }
80
+ };
81
+ const contextId = `zingit-${randomUUID()}`;
82
+ const sessionTempFiles = [];
83
+ // Build system prompt for the zingit context
84
+ const systemPrompt = `
85
+ <context>
86
+ You are a UI debugging assistant working in the project directory: ${projectDir}
87
+
88
+ When given markers about UI elements:
89
+ 1. Search for the corresponding code using the selectors and HTML context provided
90
+ 2. Make the requested changes in the project at ${projectDir}
91
+ 3. Be thorough in finding the right files and making precise edits
92
+
93
+ IMPORTANT: Format all responses using markdown.
94
+ </context>
95
+ `;
96
+ const coreSession = await this.provider.createSession({
97
+ contextId,
98
+ workingDirectory: projectDir,
99
+ systemPrompt,
100
+ resumeSessionId: resumeSessionId || undefined,
101
+ onEvent: (event) => {
102
+ const wsMsg = mapCoreEventToWS(event);
103
+ if (wsMsg)
104
+ send(wsMsg);
105
+ },
106
+ });
107
+ return {
108
+ send: async (msg) => {
109
+ try {
110
+ // For the first message we use execute(), for follow-ups we use send()
111
+ // Since zingit always calls send(), we use execute() which handles both
112
+ const attachments = imagesToAttachments(msg.images);
113
+ // If we have attachments that need temp files (for providers that need file paths),
114
+ // save them now
115
+ if (msg.images && msg.images.length > 0) {
116
+ for (const img of msg.images) {
117
+ const ext = img.mediaType.split('/')[1] || 'png';
118
+ const tempPath = path.join(os.tmpdir(), `zingit-screenshot-${randomUUID()}.${ext}`);
119
+ try {
120
+ const buffer = Buffer.from(img.base64, 'base64');
121
+ await fs.writeFile(tempPath, buffer, { mode: 0o600 });
122
+ sessionTempFiles.push(tempPath);
123
+ }
124
+ catch {
125
+ // Skip failed images
126
+ }
127
+ }
128
+ }
129
+ await coreSession.execute(msg.prompt);
130
+ }
131
+ catch (err) {
132
+ send({ type: 'error', message: err.message });
133
+ }
134
+ },
135
+ destroy: async () => {
136
+ try {
137
+ await coreSession.destroy();
138
+ }
139
+ finally {
140
+ for (const tempPath of sessionTempFiles) {
141
+ try {
142
+ await fs.unlink(tempPath);
143
+ }
144
+ catch { /* ignore */ }
145
+ }
146
+ sessionTempFiles.length = 0;
147
+ }
148
+ },
149
+ getSessionId: () => coreSession.sessionId,
150
+ };
151
+ }
152
+ }
@@ -47,6 +47,7 @@ export async function handleSelectAgent(ws, state, msg, deps) {
47
47
  }
48
48
  finally {
49
49
  state.session = null;
50
+ state.sessionId = null; // Clear stale session ID to prevent resume with wrong agent
50
51
  }
51
52
  }
52
53
  // Initialize the agent
@@ -1,26 +1,65 @@
1
1
  // server/src/index.ts
2
2
  import { WebSocketServer } from 'ws';
3
- import { CopilotAgent } from './agents/copilot.js';
4
- import { ClaudeCodeAgent } from './agents/claude.js';
5
- import { CodexAgent } from './agents/codex.js';
3
+ import { CoreProviderAdapter } from './agents/core-adapter.js';
4
+ import { CopilotProvider, ClaudeProvider, CodexProvider, } from '@codewithdan/agent-sdk-core';
5
+ import { spawn } from 'node:child_process';
6
+ import { userInfo } from 'node:os';
6
7
  import { detectAgents } from './utils/agent-detection.js';
7
8
  import { GitManager, GitManagerError } from './services/git-manager.js';
8
9
  import { sendMessage, handleGetAgents, handleSelectAgent, handleBatch, handleMessage, handleReset, handleStop, handleGetHistory, handleUndo, handleRevertTo, handleClearHistory } from './handlers/messageHandlers.js';
10
+ import { resolve } from 'path';
9
11
  const PORT = parseInt(process.env.PORT || '3000', 10);
10
12
  // Legacy support: still allow AGENT env var for backwards compatibility
11
13
  const DEFAULT_AGENT = process.env.AGENT || null;
12
- if (!process.env.PROJECT_DIR) {
13
- console.error('ERROR: PROJECT_DIR environment variable is required');
14
- console.error('Example: PROJECT_DIR=/path/to/your/project npm run dev');
15
- process.exit(1);
14
+ // Resolve PROJECT_DIR: explicit env var > npm invocation directory > cwd
15
+ const rawProjectDir = process.env.PROJECT_DIR;
16
+ const initCwd = process.env.INIT_CWD || process.cwd();
17
+ let PROJECT_DIR;
18
+ if (rawProjectDir) {
19
+ PROJECT_DIR = resolve(initCwd, rawProjectDir);
16
20
  }
17
- const PROJECT_DIR = process.env.PROJECT_DIR;
18
- // Agent registry
19
- const agentClasses = {
20
- copilot: CopilotAgent,
21
- claude: ClaudeCodeAgent,
22
- codex: CodexAgent,
23
- };
21
+ else {
22
+ PROJECT_DIR = initCwd;
23
+ console.log(`ℹ PROJECT_DIR not set, defaulting to: ${PROJECT_DIR}`);
24
+ }
25
+ // Agent registry — wraps @codewithdan/agent-sdk-core providers with zingit adapter
26
+ function createClaudeSpawner() {
27
+ if (userInfo().uid === 0) {
28
+ return {
29
+ permissionMode: 'bypassPermissions',
30
+ spawnClaudeCodeProcess: (options) => {
31
+ const { command, args, cwd, env, signal } = options;
32
+ const child = spawn('sudo', ['-u', 'ccrunner', '-E', command, ...args], {
33
+ cwd,
34
+ stdio: ['pipe', 'pipe', 'pipe'],
35
+ signal,
36
+ env: { ...env, HOME: '/home/ccrunner', USER: 'ccrunner' },
37
+ windowsHide: true,
38
+ });
39
+ return {
40
+ stdin: child.stdin,
41
+ stdout: child.stdout,
42
+ get killed() { return child.killed; },
43
+ get exitCode() { return child.exitCode; },
44
+ kill: (sig) => child.kill(sig),
45
+ on: child.on.bind(child),
46
+ once: child.once.bind(child),
47
+ off: child.off.bind(child),
48
+ };
49
+ },
50
+ };
51
+ }
52
+ return { permissionMode: 'acceptEdits' };
53
+ }
54
+ function createAgentFactory() {
55
+ const claudeOpts = createClaudeSpawner();
56
+ return {
57
+ copilot: () => new CoreProviderAdapter(new CopilotProvider()),
58
+ claude: () => new CoreProviderAdapter(new ClaudeProvider(claudeOpts)),
59
+ codex: () => new CoreProviderAdapter(new CodexProvider()),
60
+ };
61
+ }
62
+ const agentFactories = createAgentFactory();
24
63
  // Cache for initialized agents (lazy initialization)
25
64
  const initializedAgents = new Map();
26
65
  /**
@@ -33,11 +72,11 @@ async function getAgent(agentName) {
33
72
  return cached;
34
73
  }
35
74
  // Initialize new agent
36
- const AgentClass = agentClasses[agentName];
37
- if (!AgentClass) {
75
+ const factory = agentFactories[agentName];
76
+ if (!factory) {
38
77
  throw new Error(`Unknown agent: ${agentName}`);
39
78
  }
40
- const agent = new AgentClass();
79
+ const agent = factory();
41
80
  await agent.start();
42
81
  initializedAgents.set(agentName, agent);
43
82
  return agent;
@@ -135,52 +174,57 @@ async function main() {
135
174
  detectAgents,
136
175
  getAgent
137
176
  };
138
- ws.on('message', async (data) => {
139
- let msg;
140
- try {
141
- msg = JSON.parse(data.toString());
142
- }
143
- catch {
144
- sendMessage(ws, { type: 'error', message: 'Invalid JSON' });
145
- return;
146
- }
147
- try {
148
- switch (msg.type) {
149
- case 'get_agents':
150
- await handleGetAgents(ws, deps);
151
- break;
152
- case 'select_agent':
153
- await handleSelectAgent(ws, state, msg, deps);
154
- break;
155
- case 'batch':
156
- await handleBatch(ws, state, msg, deps);
157
- break;
158
- case 'message':
159
- await handleMessage(ws, state, msg, deps);
160
- break;
161
- case 'reset':
162
- await handleReset(ws, state);
163
- break;
164
- case 'stop':
165
- await handleStop(ws, state);
166
- break;
167
- case 'get_history':
168
- await handleGetHistory(ws, state);
169
- break;
170
- case 'undo':
171
- await handleUndo(ws, state, GitManagerError);
172
- break;
173
- case 'revert_to':
174
- await handleRevertTo(ws, state, msg, GitManagerError);
175
- break;
176
- case 'clear_history':
177
- await handleClearHistory(ws, state);
178
- break;
177
+ // Serialize message processing to prevent race conditions
178
+ // (e.g., undo arriving while batch finalization is still in progress)
179
+ let messageQueue = Promise.resolve();
180
+ ws.on('message', (data) => {
181
+ messageQueue = messageQueue.then(async () => {
182
+ let msg;
183
+ try {
184
+ msg = JSON.parse(data.toString());
179
185
  }
180
- }
181
- catch (err) {
182
- sendMessage(ws, { type: 'error', message: err.message });
183
- }
186
+ catch {
187
+ sendMessage(ws, { type: 'error', message: 'Invalid JSON' });
188
+ return;
189
+ }
190
+ try {
191
+ switch (msg.type) {
192
+ case 'get_agents':
193
+ await handleGetAgents(ws, deps);
194
+ break;
195
+ case 'select_agent':
196
+ await handleSelectAgent(ws, state, msg, deps);
197
+ break;
198
+ case 'batch':
199
+ await handleBatch(ws, state, msg, deps);
200
+ break;
201
+ case 'message':
202
+ await handleMessage(ws, state, msg, deps);
203
+ break;
204
+ case 'reset':
205
+ await handleReset(ws, state);
206
+ break;
207
+ case 'stop':
208
+ await handleStop(ws, state);
209
+ break;
210
+ case 'get_history':
211
+ await handleGetHistory(ws, state);
212
+ break;
213
+ case 'undo':
214
+ await handleUndo(ws, state, GitManagerError);
215
+ break;
216
+ case 'revert_to':
217
+ await handleRevertTo(ws, state, msg, GitManagerError);
218
+ break;
219
+ case 'clear_history':
220
+ await handleClearHistory(ws, state);
221
+ break;
222
+ }
223
+ }
224
+ catch (err) {
225
+ sendMessage(ws, { type: 'error', message: err.message });
226
+ }
227
+ });
184
228
  });
185
229
  ws.on('close', async () => {
186
230
  console.log('Client disconnected');