@codewithdan/zingit 0.17.3 → 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/AGENTS.md +17 -14
- package/CHANGELOG.md +23 -1
- package/README.md +10 -18
- package/client/dist/zingit-client.js +207 -11
- package/package.json +21 -20
- package/server/dist/agents/core-adapter.d.ts +16 -0
- package/server/dist/agents/core-adapter.js +152 -0
- package/server/dist/handlers/messageHandlers.js +1 -0
- package/server/dist/index.js +106 -62
package/package.json
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codewithdan/zingit",
|
|
3
|
-
"version": "0.17.
|
|
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": "
|
|
25
|
-
"build:server": "
|
|
26
|
-
"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": "
|
|
29
|
-
"dev:client": "
|
|
30
|
-
"test": "
|
|
31
|
-
"test:watch": "
|
|
32
|
-
"test:ui": "
|
|
33
|
-
"release": "
|
|
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": "
|
|
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
|
-
"
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"
|
|
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
|
+
}
|
package/server/dist/index.js
CHANGED
|
@@ -1,26 +1,65 @@
|
|
|
1
1
|
// server/src/index.ts
|
|
2
2
|
import { WebSocketServer } from 'ws';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
37
|
-
if (!
|
|
75
|
+
const factory = agentFactories[agentName];
|
|
76
|
+
if (!factory) {
|
|
38
77
|
throw new Error(`Unknown agent: ${agentName}`);
|
|
39
78
|
}
|
|
40
|
-
const agent =
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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
|
-
|
|
182
|
-
|
|
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');
|