wtt-connect 0.2.50 → 0.2.52

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.50",
3
+ "version": "0.2.52",
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/main.js CHANGED
@@ -20,6 +20,7 @@ export async function main(args) {
20
20
  const argv = parseArgs(args.slice(1));
21
21
  if (!argv.envFile && argv.profile) argv.envFile = resolveProfileEnvFile(argv.profile);
22
22
  loadDefaultEnvFiles({ envFile: argv.envFile });
23
+ if (cmd === 'version' || cmd === '--version' || cmd === '-v') return printVersion();
23
24
  if (cmd === 'help' || cmd === '--help' || cmd === '-h') return printHelp();
24
25
  if (cmd === 'up' || cmd === 'link') return up(argv);
25
26
  if (cmd === 'list') return listProfiles(argv);
@@ -109,6 +110,15 @@ function parseArgs(args) {
109
110
  return out;
110
111
  }
111
112
 
113
+ function printVersion() {
114
+ try {
115
+ const pkg = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
116
+ console.log(pkg.version || 'unknown');
117
+ } catch {
118
+ console.log('unknown');
119
+ }
120
+ }
121
+
112
122
  function printHelp() {
113
123
  console.log(`wtt-connect
114
124
 
@@ -138,7 +148,7 @@ Commands:
138
148
  opendesign-upload --dir <path>
139
149
  Alias for upload-artifact
140
150
  preview-port --port <port> [--topic-id <id>] [--snapshot-dir <dir>]
141
- Create a Cloud Sandbox preview URL and optional persistent snapshot
151
+ Create a Cloud Sandbox live preview URL; snapshots are opt-in
142
152
  cleanup-previews Stop preview servers previously registered by this agent
143
153
  help Show this help
144
154
  `);
@@ -163,7 +173,8 @@ async function previewPort(config, argv) {
163
173
  const url = preview.preview_url || preview.url;
164
174
  if (!url) throw new Error('Cloud Sandbox preview API returned no URL');
165
175
  const title = String(argv.title || argv.previewName || preview.name || 'Cloud Sandbox Preview').trim();
166
- const snapshot = argv.noSnapshot ? null : await createPreviewSnapshot(config, argv, { title });
176
+ const wantsSnapshot = Boolean(String(argv.snapshotDir || '').trim()) && !argv.noSnapshot;
177
+ const snapshot = wantsSnapshot ? await createPreviewSnapshot(config, argv, { title }) : null;
167
178
  const snapshotUrl = snapshot?.snapshot_url || '';
168
179
  const markdown = JSON.stringify({
169
180
  type: 'cloud_sandbox_preview',
package/src/runner.js CHANGED
@@ -943,11 +943,12 @@ function renderCloudSandboxStorageInstruction(config, topicId = '') {
943
943
  '- 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`.',
944
944
  '- If local curl fails, fix or restart the web server first. Never publish a preview URL for a dead port.',
945
945
  '- `wtt-connect preview-port` automatically keeps the most recent preview servers for this agent and stops older registered previews beyond the retention limit. Default retention is 3 live previews; use `--keep-last <n>` only when the user asks.',
946
- '- For static pages, animations, charts, dashboards, and HTML demos, pass the generated static directory to `wtt-connect preview-port --snapshot-dir <dir> --snapshot-entry index.html` so WTT stores a persistent snapshot fallback. Live preview URLs are for recent interactive previews; snapshots/artifacts preserve history after cleanup.',
946
+ '- Do not pass `--snapshot-dir` for normal live previews. Live Cloud Sandbox previews should render as inline WTT preview cards, not as ordinary index.html/document artifacts.',
947
+ '- If the user explicitly asks for a persistent static artifact or downloadable HTML, publish that separately with `wtt-connect upload-artifact --dir <dir> --title "Short Title"` instead of mixing it with the live preview URL.',
947
948
  '- 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>)`.',
948
949
  '- Preview ports must be 1024-65535 and cannot be 3000. For Vite/Next-style dev servers prefer 5173, 4173, or 8080.',
949
950
  `- 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>}'`,
950
- `- Prefer automatic WTT publishing when topic id is available: wtt-connect preview-port --port <port>${topicId ? ` --topic-id ${topicId}` : ' --topic-id <topic_id>'} --title "Short Title" --snapshot-dir <static_dir> --snapshot-entry index.html. This publishes a cloud_sandbox_preview card with live and snapshot URLs.`,
951
+ `- Prefer automatic WTT publishing when topic id is available: wtt-connect preview-port --port <port>${topicId ? ` --topic-id ${topicId}` : ' --topic-id <topic_id>'} --title "Short Title". This publishes one cloud_sandbox_preview live card.`,
951
952
  );
952
953
  return lines.join('\n');
953
954
  }
@@ -87,12 +87,16 @@ export async function up(argv) {
87
87
 
88
88
  export function listProfiles() {
89
89
  const dir = profilesDir();
90
+ const sandboxEntries = discoverCloudSandboxEntries();
90
91
  if (!fs.existsSync(dir)) {
91
- console.log('no profiles');
92
+ if (!sandboxEntries.length) console.log('no profiles');
93
+ for (const entry of sandboxEntries) {
94
+ console.log(`${entry.profile}\t${entry.adapter || 'cloud-sandbox'}\t${entry.agentId}\tcloud-sandbox`);
95
+ }
92
96
  return;
93
97
  }
94
98
  const files = fs.readdirSync(dir).filter((f) => f.endsWith('.env')).sort();
95
- if (!files.length) {
99
+ if (!files.length && !sandboxEntries.length) {
96
100
  console.log('no profiles');
97
101
  return;
98
102
  }
@@ -101,23 +105,36 @@ export function listProfiles() {
101
105
  const env = readEnv(path.join(dir, file));
102
106
  console.log(`${profile}\t${env.WTT_CONNECT_ADAPTER || ''}\t${env.WTT_AGENT_ID || ''}`);
103
107
  }
108
+ for (const entry of sandboxEntries) {
109
+ console.log(`${entry.profile}\t${entry.adapter || 'cloud-sandbox'}\t${entry.agentId}\tcloud-sandbox`);
110
+ }
104
111
  }
105
112
 
106
113
  export function status(argv) {
107
- const profiles = targetProfiles(argv);
108
- if (!profiles.length) {
114
+ const profiles = targetProfiles(argv, { allowEmpty: true });
115
+ const sandboxEntries = targetCloudSandboxEntries(argv);
116
+ if (!profiles.length && !sandboxEntries.length) {
109
117
  console.log('no profiles');
110
118
  return;
111
119
  }
112
120
  for (const profile of profiles) {
113
121
  console.log(`${profile}: ${serviceStatus(profile)}`);
114
122
  }
123
+ for (const entry of sandboxEntries) {
124
+ console.log(`${entry.profile}: ${cloudSandboxStatus(entry)}`);
125
+ }
115
126
  }
116
127
 
117
128
  export function restart(argv) {
118
- const profiles = targetProfiles(argv);
129
+ const profiles = targetProfiles(argv, { allowEmpty: true });
130
+ const sandboxEntries = targetCloudSandboxEntries(argv);
131
+ if (!profiles.length && !sandboxEntries.length) {
132
+ console.log('no profiles');
133
+ return;
134
+ }
119
135
  for (const profile of profiles) restartService(profile);
120
- status({ _: profiles });
136
+ for (const entry of sandboxEntries) restartCloudSandboxEntry(entry);
137
+ status({ _: [...profiles, ...sandboxEntries.map((entry) => entry.profile)] });
121
138
  }
122
139
 
123
140
  export function logs(argv) {
@@ -182,11 +199,17 @@ export function down(argv) {
182
199
  console.log(`kept state dir: ${defaultStateDir(safe)}`);
183
200
  }
184
201
 
185
- function targetProfiles(argv) {
202
+ function targetProfiles(argv, options = {}) {
186
203
  const requested = argv._ || [];
187
- if (requested.length && requested[0] !== 'all') return requested.map(sanitizeProfile);
204
+ if (requested.length && requested[0] !== 'all') {
205
+ const available = new Set(discoverProfiles());
206
+ const profiles = requested.map(sanitizeProfile).filter((profile) => available.has(profile));
207
+ if (profiles.length || options.allowEmpty) return profiles;
208
+ return requested.map(sanitizeProfile);
209
+ }
188
210
  const profiles = discoverProfiles();
189
211
  if (!profiles.length && requested[0] === 'all') return [];
212
+ if (!profiles.length && options.allowEmpty) return [];
190
213
  if (!profiles.length) throw new Error('no wtt-connect profiles found');
191
214
  return profiles;
192
215
  }
@@ -197,6 +220,132 @@ function discoverProfiles() {
197
220
  return fs.readdirSync(dir).filter((f) => f.endsWith('.env')).map((f) => sanitizeProfile(f.slice(0, -4))).sort();
198
221
  }
199
222
 
223
+ function targetCloudSandboxEntries(argv) {
224
+ const requested = (argv._ || []).map((value) => String(value || '').trim()).filter(Boolean);
225
+ const entries = discoverCloudSandboxEntries();
226
+ if (!requested.length || requested[0] === 'all') return entries;
227
+ const wanted = new Set(requested.map(sanitizeProfile));
228
+ return entries.filter((entry) => wanted.has(entry.profile) || wanted.has(entry.agentId));
229
+ }
230
+
231
+ function discoverCloudSandboxEntries() {
232
+ const root = path.join(os.homedir(), '.wtt-connect', 'agent');
233
+ if (!fs.existsSync(root)) return [];
234
+ const entries = [];
235
+ for (const name of fs.readdirSync(root).sort()) {
236
+ const agentRoot = path.join(root, name);
237
+ const envFile = path.join(agentRoot, 'run', 'wtt-connect.env');
238
+ if (!fs.existsSync(envFile)) continue;
239
+ const env = readEnv(envFile);
240
+ const agentId = sanitizeProfile(env.WTT_AGENT_ID || name);
241
+ if (!agentId) continue;
242
+ entries.push({
243
+ profile: agentId,
244
+ agentId,
245
+ adapter: env.WTT_CONNECT_ADAPTER || env.WTT_CONNECT_ADAPTERS || '',
246
+ envFile,
247
+ agentRoot,
248
+ pidFile: path.join(agentRoot, 'run', 'wtt-connect.pid'),
249
+ logFile: path.join(agentRoot, 'logs', 'wtt-connect.log'),
250
+ workDir: env.WTT_CONNECT_WORKDIR || path.join(agentRoot, 'workspace'),
251
+ });
252
+ }
253
+ return entries;
254
+ }
255
+
256
+ function cloudSandboxStatus(entry) {
257
+ const pid = readPid(entry.pidFile);
258
+ if (pid && processRunningWithAgent(pid, entry.agentId)) return `active pid=${pid}`;
259
+ const found = findPidByAgentEnv(entry.agentId);
260
+ if (found) return `active pid=${found}`;
261
+ return 'inactive';
262
+ }
263
+
264
+ function restartCloudSandboxEntry(entry) {
265
+ stopCloudSandboxEntry(entry);
266
+ ensureDir(path.dirname(entry.pidFile));
267
+ ensureDir(path.dirname(entry.logFile));
268
+ ensureDir(entry.workDir);
269
+ const bin = path.join(packageRoot(), 'bin', 'wtt-connect.js');
270
+ const script = [
271
+ `set -a`,
272
+ `. ${shellQuote(entry.envFile)}`,
273
+ `set +a`,
274
+ `export PATH=${shellQuote(runtimePath())}`,
275
+ `mkdir -p ${shellQuote(entry.workDir)} ${shellQuote(path.dirname(entry.pidFile))} ${shellQuote(path.dirname(entry.logFile))}`,
276
+ `cd ${shellQuote(entry.workDir)}`,
277
+ `nohup ${shellQuote(process.execPath)} ${shellQuote(bin)} start --env-file ${shellQuote(entry.envFile)} >> ${shellQuote(entry.logFile)} 2>&1 < /dev/null &`,
278
+ `echo $! > ${shellQuote(entry.pidFile)}`,
279
+ ].join('; ');
280
+ const result = spawnSync('bash', ['-lc', script], { encoding: 'utf8' });
281
+ if (result.status !== 0) {
282
+ throw new Error(`cloud sandbox restart failed for ${entry.agentId}: ${result.stderr || result.stdout}`);
283
+ }
284
+ }
285
+
286
+ function stopCloudSandboxEntry(entry) {
287
+ const pids = new Set();
288
+ const pid = readPid(entry.pidFile);
289
+ if (pid) pids.add(pid);
290
+ const envPid = findPidByAgentEnv(entry.agentId);
291
+ if (envPid) pids.add(envPid);
292
+ for (const target of pids) {
293
+ try { process.kill(target, 'SIGTERM'); } catch {}
294
+ }
295
+ const deadline = Date.now() + 2000;
296
+ while (Date.now() < deadline && Array.from(pids).some((target) => processAlive(target))) {
297
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
298
+ }
299
+ for (const target of pids) {
300
+ if (!processAlive(target)) continue;
301
+ try { process.kill(target, 'SIGKILL'); } catch {}
302
+ }
303
+ try { fs.rmSync(entry.pidFile); } catch {}
304
+ }
305
+
306
+ function readPid(file) {
307
+ try {
308
+ const pid = Number.parseInt(fs.readFileSync(file, 'utf8').trim(), 10);
309
+ return Number.isFinite(pid) && pid > 0 ? pid : 0;
310
+ } catch {
311
+ return 0;
312
+ }
313
+ }
314
+
315
+ function processAlive(pid) {
316
+ try {
317
+ process.kill(pid, 0);
318
+ return true;
319
+ } catch {
320
+ return false;
321
+ }
322
+ }
323
+
324
+ function processRunningWithAgent(pid, agentId) {
325
+ if (!processAlive(pid)) return false;
326
+ try {
327
+ const env = fs.readFileSync(`/proc/${pid}/environ`, 'utf8').split('\0');
328
+ return env.includes(`WTT_AGENT_ID=${agentId}`);
329
+ } catch {
330
+ return false;
331
+ }
332
+ }
333
+
334
+ function findPidByAgentEnv(agentId) {
335
+ if (process.platform !== 'linux') return 0;
336
+ for (const name of fs.readdirSync('/proc')) {
337
+ if (!/^\d+$/.test(name)) continue;
338
+ const pid = Number.parseInt(name, 10);
339
+ if (pid === process.pid) continue;
340
+ if (processRunningWithAgent(pid, agentId)) return pid;
341
+ }
342
+ return 0;
343
+ }
344
+
345
+ function shellQuote(value) {
346
+ return `'${String(value).replace(/'/g, `'\\''`)}'`;
347
+ }
348
+
200
349
  function writeProfileEnv(file, values) {
201
350
  const lines = [
202
351
  '# wtt-connect profile',