wtt-connect 0.2.39 → 0.2.41

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": "wtt-connect",
3
- "version": "0.2.39",
3
+ "version": "0.2.41",
4
4
  "private": false,
5
5
  "description": "WTT-native connector daemon for Codex, Claude Code, Cursor, Gemini, ACP, and other coding agent surfaces.",
6
6
  "type": "module",
package/src/events.js CHANGED
@@ -20,6 +20,9 @@ export function normalizeAgentEvent(adapterName, event) {
20
20
  }
21
21
  }
22
22
 
23
+ const generic = normalizeGenericCliEvent(event, now);
24
+ if (generic) return generic;
25
+
23
26
  if (type === 'item.started') {
24
27
  const kind = item.type || 'item';
25
28
  if (kind === 'command_execution') return { kind: 'command', status: 'started', text: item.command || 'command', time: now, raw: event };
@@ -35,6 +38,103 @@ export function normalizeAgentEvent(adapterName, event) {
35
38
  return null;
36
39
  }
37
40
 
41
+ function normalizeGenericCliEvent(event, now) {
42
+ if (!event || typeof event !== 'object') return null;
43
+ const type = String(event.type || event.event || event.kind || '').trim();
44
+ const status = statusFromEvent(event, type);
45
+ const item = event.item || event.delta || event.message || event.data || {};
46
+ const typeText = `${type} ${String(item.type || item.kind || '').trim()}`.toLowerCase();
47
+
48
+ const error = event.error?.message || event.error || event.message?.error || '';
49
+ if (error && (typeText.includes('error') || String(error).trim())) {
50
+ return { kind: 'error', status: 'failed', text: String(error), time: now, raw: event };
51
+ }
52
+
53
+ const command = firstString(
54
+ event.command,
55
+ event.cmd,
56
+ event.args && Array.isArray(event.args) ? event.args.join(' ') : '',
57
+ item.command,
58
+ item.cmd,
59
+ item.input?.command,
60
+ item.arguments?.command,
61
+ );
62
+ if (command || includesAny(typeText, ['command', 'exec', 'shell', 'terminal'])) {
63
+ return { kind: 'command', status, text: command || labelFromEvent(event, item) || 'command', time: now, raw: event };
64
+ }
65
+
66
+ const toolName = firstString(
67
+ event.tool,
68
+ event.tool_name,
69
+ event.name,
70
+ event.call?.name,
71
+ item.tool,
72
+ item.tool_name,
73
+ item.name,
74
+ item.call?.name,
75
+ item.function?.name,
76
+ );
77
+ if (toolName || includesAny(typeText, ['tool', 'function_call', 'mcp'])) {
78
+ const summary = summarizeToolInput(event.input || item.input || item.arguments || event.arguments);
79
+ return { kind: 'tool', status, text: summary ? `${toolName || 'tool'} ${summary}` : (toolName || labelFromEvent(event, item) || 'tool'), time: now, raw: event };
80
+ }
81
+
82
+ const query = firstString(event.query, item.query, event.search_query, item.search_query);
83
+ if (query || includesAny(typeText, ['web_search', 'search'])) {
84
+ return { kind: 'web_search', status, text: query || labelFromEvent(event, item) || 'web search', time: now, raw: event };
85
+ }
86
+
87
+ if (includesAny(typeText, ['reason', 'thinking', 'analysis'])) {
88
+ return { kind: 'reasoning', status, text: labelFromEvent(event, item) || 'thinking', time: now, raw: event };
89
+ }
90
+
91
+ if (includesAny(typeText, ['message', 'assistant', 'response', 'output', 'answer'])) {
92
+ const text = firstString(event.text, event.content, event.delta, item.text, item.content);
93
+ return { kind: 'response', status, text: text ? String(text).slice(0, 120) : 'writing response', time: now, raw: event };
94
+ }
95
+
96
+ if (includesAny(typeText, ['session', 'thread', 'turn'])) {
97
+ return { kind: 'session', status, text: labelFromEvent(event, item) || 'session', time: now, raw: event };
98
+ }
99
+
100
+ return null;
101
+ }
102
+
103
+ function statusFromEvent(event, type) {
104
+ const raw = `${String(event.status || event.state || '')} ${type}`.toLowerCase();
105
+ if (includesAny(raw, ['fail', 'error'])) return 'failed';
106
+ if (includesAny(raw, ['complete', 'completed', 'done', 'finish', 'finished', 'end', 'ended', 'stop', 'stopped'])) return 'completed';
107
+ return 'started';
108
+ }
109
+
110
+ function labelFromEvent(event, item) {
111
+ return firstString(
112
+ event.title,
113
+ event.label,
114
+ event.message,
115
+ event.summary,
116
+ item.title,
117
+ item.label,
118
+ item.message,
119
+ item.summary,
120
+ item.type,
121
+ event.type,
122
+ );
123
+ }
124
+
125
+ function firstString(...values) {
126
+ for (const value of values) {
127
+ if (value == null) continue;
128
+ if (typeof value === 'string' && value.trim()) return value.trim();
129
+ if (typeof value === 'number' || typeof value === 'boolean') return String(value);
130
+ }
131
+ return '';
132
+ }
133
+
134
+ function includesAny(value, needles) {
135
+ return needles.some((needle) => value.includes(needle));
136
+ }
137
+
38
138
  export function renderStatusLine(evt) {
39
139
  if (!evt) return '';
40
140
  return `[TASK_STATUS] time=${evt.time} status=${evt.status} action=${evt.kind}:${String(evt.text || '').slice(0, 180)}`;
@@ -49,6 +149,7 @@ export function renderActivityText(evt, adapterName = 'agent') {
49
149
  if (evt.kind === 'command') return `${adapter} 正在执行命令:${text || 'command'}`;
50
150
  if (evt.kind === 'tool') return `${adapter} 正在调用工具:${text || 'tool'}`;
51
151
  if (evt.kind === 'web_search') return `${adapter} 正在搜索:${text || 'web search'}`;
152
+ if (evt.kind === 'reasoning') return `${adapter} 正在推理:${text || 'thinking'}`;
52
153
  if (evt.kind === 'response') return `${adapter} 正在组织回复`;
53
154
  if (evt.kind === 'session') return `${adapter} 会话已启动`;
54
155
  return `${adapter} 正在执行:${text || labelKind(evt.kind)}`;
package/src/main.js CHANGED
@@ -36,6 +36,7 @@ export async function main(args) {
36
36
  if (cmd === 'upload-file' || cmd === 'send-file') return uploadFile(loadConfig(argv), argv);
37
37
  if (cmd === 'upload-artifact' || cmd === 'opendesign-upload') return uploadArtifact(loadConfig(argv), argv);
38
38
  if (cmd === 'preview-port' || cmd === 'sandbox-preview') return previewPort(loadConfig(argv), argv);
39
+ if (cmd === 'cleanup-previews' || cmd === 'preview-cleanup') return cleanupPreviewsCommand(loadConfig(argv), argv);
39
40
  if (cmd === 'start') {
40
41
  const config = loadConfig(argv);
41
42
  if (!config.agentId) throw new Error('WTT_AGENT_ID is required');
@@ -95,6 +96,7 @@ function parseArgs(args) {
95
96
  else if (a === '--port') out.port = Number(args[++i]);
96
97
  else if (a === '--preview-name') out.previewName = args[++i];
97
98
  else if (a === '--preview-token') out.previewToken = args[++i];
99
+ else if (a === '--keep-last') out.keepLast = Number(args[++i]);
98
100
  else if (a === '--timeout') out.timeout = Number(args[++i]) * 1000;
99
101
  else if (a === '--sender-agent-id') out.senderAgentId = args[++i];
100
102
  else if (a === '--sender-token') out.senderToken = args[++i];
@@ -134,6 +136,7 @@ Commands:
134
136
  Alias for upload-artifact
135
137
  preview-port --port <port> [--topic-id <id>]
136
138
  Create a Cloud Sandbox port preview URL through sandbox outbox
139
+ cleanup-previews Stop preview servers previously registered by this agent
137
140
  help Show this help
138
141
  `);
139
142
  }
@@ -143,6 +146,10 @@ async function previewPort(config, argv) {
143
146
  if (!Number.isInteger(port) || port < 1024 || port > 65535 || port === 3000) {
144
147
  throw new Error('preview-port requires --port <1024-65535>, excluding 3000');
145
148
  }
149
+ const cleanup = cleanupOldPreviews(config, {
150
+ currentPort: port,
151
+ keepLast: Number.isFinite(Number(argv.keepLast)) ? Number(argv.keepLast) : 0,
152
+ });
146
153
  const preview = await createSandboxPreviewFromOutbox(config, port, {
147
154
  name: String(argv.previewName || argv.title || '').trim(),
148
155
  token: String(argv.previewToken || '').trim(),
@@ -162,12 +169,89 @@ async function previewPort(config, argv) {
162
169
  type: 'cloud_sandbox_preview',
163
170
  port,
164
171
  preview_url: url,
172
+ preview_cleanup: cleanup,
165
173
  });
166
174
  } finally {
167
175
  await wtt.close();
168
176
  }
169
177
  }
170
- console.log(JSON.stringify({ ok: true, port, url, preview_url: url, markdown, published }, null, 2));
178
+ recordPreview(config, { port, url, name: preview.name || title });
179
+ console.log(JSON.stringify({ ok: true, port, url, preview_url: url, markdown, cleanup, published }, null, 2));
180
+ }
181
+
182
+ async function cleanupPreviewsCommand(config, argv) {
183
+ const keepLast = Number.isFinite(Number(argv.keepLast)) ? Number(argv.keepLast) : 0;
184
+ const cleanup = cleanupOldPreviews(config, { currentPort: 0, keepLast });
185
+ console.log(JSON.stringify({ ok: true, cleanup }, null, 2));
186
+ }
187
+
188
+ function cleanupOldPreviews(config, { currentPort = 0, keepLast = 0 } = {}) {
189
+ const registry = readPreviewRegistry(config);
190
+ const entries = registry
191
+ .filter((entry) => Number(entry.port) !== Number(currentPort))
192
+ .sort((a, b) => Number(b.created_at_ms || 0) - Number(a.created_at_ms || 0));
193
+ const keep = Math.max(0, Number(keepLast) || 0);
194
+ const toKeep = entries.slice(0, keep);
195
+ const toStop = entries.slice(keep);
196
+ const stopped = [];
197
+ for (const entry of toStop) {
198
+ const result = stopPreviewPort(Number(entry.port));
199
+ stopped.push({ port: Number(entry.port), url: entry.url || '', ...result });
200
+ }
201
+ writePreviewRegistry(config, currentPort ? toKeep : toKeep);
202
+ return { stopped, kept: toKeep.map((entry) => ({ port: Number(entry.port), url: entry.url || '' })) };
203
+ }
204
+
205
+ function recordPreview(config, entry) {
206
+ const registry = readPreviewRegistry(config).filter((item) => Number(item.port) !== Number(entry.port));
207
+ registry.unshift({
208
+ port: Number(entry.port),
209
+ url: String(entry.url || ''),
210
+ name: String(entry.name || ''),
211
+ created_at: new Date().toISOString(),
212
+ created_at_ms: Date.now(),
213
+ });
214
+ writePreviewRegistry(config, registry.slice(0, 10));
215
+ }
216
+
217
+ function previewRegistryFile(config) {
218
+ return path.join(config.stateDir || path.join(config.workDir || process.cwd(), '.wtt-connect'), 'previews.json');
219
+ }
220
+
221
+ function readPreviewRegistry(config) {
222
+ try {
223
+ const file = previewRegistryFile(config);
224
+ if (!fs.existsSync(file)) return [];
225
+ const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
226
+ return Array.isArray(parsed) ? parsed : [];
227
+ } catch {
228
+ return [];
229
+ }
230
+ }
231
+
232
+ function writePreviewRegistry(config, registry) {
233
+ const file = previewRegistryFile(config);
234
+ fs.mkdirSync(path.dirname(file), { recursive: true });
235
+ fs.writeFileSync(file, `${JSON.stringify(registry, null, 2)}\n`);
236
+ }
237
+
238
+ function stopPreviewPort(port) {
239
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) return { killed: [], reason: 'invalid_port' };
240
+ const script = [
241
+ `pids=$(ss -ltnp 2>/dev/null | awk '/:${port} / {print $NF}' | sed -n 's/.*pid=\\([0-9][0-9]*\\).*/\\1/p' | sort -u)`,
242
+ 'killed=""',
243
+ 'for pid in $pids; do',
244
+ ' if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill "$pid" 2>/dev/null || true; killed="$killed $pid"; fi',
245
+ 'done',
246
+ 'sleep 0.2',
247
+ 'for pid in $pids; do',
248
+ ' if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then kill -9 "$pid" 2>/dev/null || true; fi',
249
+ 'done',
250
+ 'printf "%s" "$killed"',
251
+ ].join('; ');
252
+ const result = spawnSync('bash', ['-lc', script], { encoding: 'utf8', timeout: 5000 });
253
+ const killed = String(result.stdout || '').trim().split(/\s+/).filter(Boolean);
254
+ return { killed, ok: result.status === 0 };
171
255
  }
172
256
 
173
257
  async function createSandboxPreviewFromOutbox(config, port, { name = '', token = '', timeoutMs = 15000 } = {}) {
package/src/runner.js CHANGED
@@ -939,6 +939,11 @@ function renderCloudSandboxStorageInstruction(config, topicId = '') {
939
939
  '- If a generated user-facing file is stored in R2/persistent output storage and should appear in WTT chat, still publish it with `wtt-connect upload-file` or a WTT artifact marker.',
940
940
  '- If the user asks for a visual page, HTML artifact, chart, dashboard, animation, or browser preview, build it in the workspace, start a local web server, create a Cloudflare Sandbox preview URL, and return that preview URL to WTT.',
941
941
  '- The Cloud Sandbox preview URL feature is only for WTT Cloud Sandbox agents. Do not use WTT backend artifact/media preview flows for live sandbox web servers.',
942
+ '- Before starting a new preview server, run `wtt-connect cleanup-previews` to stop older preview servers for this agent and free sandbox resources.',
943
+ '- Preview servers must keep running after your command finishes. Start them in the background with `nohup ... >/tmp/<name>.log 2>&1 &` or an equivalent long-lived process, and bind to `0.0.0.0`.',
944
+ '- Before publishing a preview URL, verify the server is actually listening and serving content with `curl -fsS http://127.0.0.1:<port>/ >/dev/null`.',
945
+ '- If local curl fails, fix or restart the web server first. Never publish a preview URL for a dead port.',
946
+ '- `wtt-connect preview-port` automatically stops older preview servers registered by this agent before publishing the new preview, so prefer one active preview per agent unless the user asks otherwise.',
942
947
  '- If you start a web server in the sandbox and the user should preview it, call the Cloudflare Sandbox outbound Worker directly with curl and include the returned `preview_url` in your reply as `[preview_url:Short Title](<preview_url>)`.',
943
948
  '- Preview ports must be 1024-65535 and cannot be 3000. For Vite/Next-style dev servers prefer 5173, 4173, or 8080.',
944
949
  `- Preview curl rule: curl -sS -X POST "\${WTT_SANDBOX_OUTBOX_URL:-http://wtt.preview}/preview-port" -H 'content-type: application/json' -d '{"agent_id":"'\${WTT_AGENT_ID:-cloud-agent}'","port":<port>}'`,