wtt-connect 0.2.11 → 0.2.14

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/README.md CHANGED
@@ -21,7 +21,6 @@ Implemented production-oriented surfaces:
21
21
  - `acp` for any Agent Client Protocol compatible agent
22
22
  - `devin` through ACP (`devin acp`)
23
23
  - Normalized tool/progress event publishing
24
- - OpenDesign visual artifact generation for formula/principle/process/UI explanations, with a quality gate that checks for dense structure, SVG/canvas diagrams, animation, examples, and formula/metric explanation before upload
25
24
  - TTS extension point:
26
25
  - `none`
27
26
  - `macos-say` using the macOS `say` command, emitted as WAV when upload is enabled
@@ -122,10 +121,19 @@ Codex, Claude Code, and other adapters can generate user-facing files on the age
122
121
  Supported generated artifact types:
123
122
 
124
123
  ```text
125
- .doc .docx .ppt .pptx .xls .xlsx .pdf .csv .md .txt .zip
124
+ .doc .docx .ppt .pptx .xls .xlsx .pdf .csv .md .txt .zip .html .png .jpg .webp .mp4 .webm .mov
126
125
  ```
127
126
 
128
- When an agent creates a deliverable file, it should save the file under its workspace or artifact directory and include one final-response marker per file:
127
+ When an agent creates a deliverable file, it can save the file anywhere under its own WTT workspace (`~/.wtt-connect/<agentID>` by default, or `WTT_CONNECT_WORKDIR` when set). The agent should then explicitly publish it with:
128
+
129
+ ```bash
130
+ wtt-connect upload-file --topic-id <topic_id> --title "Project Report" ./report.pdf
131
+ wtt-connect upload-file --topic-id <topic_id> --title "Design Pack" ./a.pdf ./slides.pptx ./notes.md
132
+ ```
133
+
134
+ `upload-file` uploads the file through WTT media and publishes a clickable file card into the topic. If several files are passed, it packages them into one zip before publishing. `wtt-connect` does not scan the workspace automatically.
135
+
136
+ Agents can also include one final-response marker per file to provide a better display title:
129
137
 
130
138
  ```text
131
139
  [WTT_ARTIFACT_FILE path="report.docx" title="Project Report"]
@@ -140,7 +148,7 @@ When an agent creates a deliverable file, it should save the file under its work
140
148
 
141
149
  Notes:
142
150
 
143
- - Generated file markers are explicit user-facing uploads; they are uploaded even when `WTT_CONNECT_UPLOAD_ARTIFACTS=0`.
151
+ - Generated files published through `upload-file` and explicit markers are user-facing uploads; they are uploaded even when `WTT_CONNECT_UPLOAD_ARTIFACTS=0`.
144
152
  - Files outside the configured workspace, state directory, inbox directory, or artifact directory are ignored to avoid accidental leakage.
145
153
  - `WTT_CONNECT_UPLOAD_ARTIFACTS=1` still controls automatic summary/TTS uploads; it is not required for explicit generated file markers.
146
154
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtt-connect",
3
- "version": "0.2.11",
3
+ "version": "0.2.14",
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
@@ -1,4 +1,6 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
2
4
  import { loadDefaultEnvFiles } from './env.js';
3
5
  import { loadConfig } from './config.js';
4
6
  import { Runner } from './runner.js';
@@ -7,6 +9,7 @@ import { setup } from './setup.js';
7
9
  import { smokeChat, smokeTask } from './smoke.js';
8
10
  import { ArtifactManager } from './artifacts.js';
9
11
  import { DurableStore } from './store.js';
12
+ import { WTTClient } from './wtt-client.js';
10
13
  import { log, redact } from './logger.js';
11
14
  import { adapterBin, normalizeProfileName } from './adapters/generic-cli.js';
12
15
  import { normalizeAdapterName } from './adapters/index.js';
@@ -30,6 +33,7 @@ export async function main(args) {
30
33
  if (cmd === 'claim-code') return setup(loadConfig(argv), { ...argv, noRegister: true, claimCode: true });
31
34
  if (cmd === 'smoke-task') return smokeTask(loadConfig(argv), argv);
32
35
  if (cmd === 'smoke-chat') return smokeChat(loadConfig(argv), argv);
36
+ if (cmd === 'upload-file' || cmd === 'send-file') return uploadFile(loadConfig(argv), argv);
33
37
  if (cmd === 'upload-artifact' || cmd === 'opendesign-upload') return uploadArtifact(loadConfig(argv), argv);
34
38
  if (cmd === 'start') {
35
39
  const config = loadConfig(argv);
@@ -73,6 +77,14 @@ function parseArgs(args) {
73
77
  else if (a === '--expected') out.expected = args[++i];
74
78
  else if (a === '--prompt') out.prompt = args[++i];
75
79
  else if (a === '--message') out.message = args[++i];
80
+ else if (a === '--topic-id' || a === '--topic') out.topicId = args[++i];
81
+ else if (a === '--file') {
82
+ if (!out.files) out.files = [];
83
+ out.files.push(args[++i]);
84
+ }
85
+ else if (a === '--zip') out.zip = true;
86
+ else if (a === '--no-zip') out.noZip = true;
87
+ else if (a === '--semantic-type') out.semanticType = args[++i];
76
88
  else if (a === '--dir') out.dir = args[++i];
77
89
  else if (a === '--source') out.source = args[++i];
78
90
  else if (a === '--source-id') out.sourceId = args[++i];
@@ -111,6 +123,8 @@ Commands:
111
123
  smoke-task Create/run a WTT task and wait for connector result
112
124
  smoke-chat Send P2P chat from smoke sender and wait for reply
113
125
  claim-code Print claim code for current WTT_AGENT_ID/TOKEN
126
+ upload-file --topic-id <id> <file...>
127
+ Upload generated file(s) and publish WTT file card
114
128
  upload-artifact --dir <path> Upload an OpenDesign/artifact directory to WTT
115
129
  opendesign-upload --dir <path>
116
130
  Alias for upload-artifact
@@ -118,6 +132,83 @@ Commands:
118
132
  `);
119
133
  }
120
134
 
135
+ async function uploadFile(config, argv) {
136
+ const topicId = String(argv.topicId || argv.sourceId || '').trim();
137
+ if (!topicId) throw new Error('upload-file requires --topic-id <topic_id>');
138
+ const rawFiles = [...(argv.files || []), ...(argv._ || [])].map((file) => String(file || '').trim()).filter(Boolean);
139
+ if (!rawFiles.length) throw new Error('upload-file requires at least one file path');
140
+ const files = rawFiles.map((file) => resolveUploadFilePath(file, config));
141
+ for (const file of files) {
142
+ const stat = fs.statSync(file);
143
+ if (!stat.isFile()) throw new Error(`not a file: ${file}`);
144
+ }
145
+
146
+ const store = new DurableStore(config.storeFile).load();
147
+ const artifacts = new ArtifactManager(config, store);
148
+ const wtt = new WTTClient(config, async () => {});
149
+ try {
150
+ await wtt.connectForActions();
151
+ const target = files.length > 1 || argv.zip ? packageUploadFiles(files, config, argv) : { path: files[0], originalFiles: files };
152
+ const stat = fs.statSync(target.path);
153
+ const asset = await artifacts.uploadFile(target.path, { force: true });
154
+ const url = asset?.url || asset?.public_url;
155
+ if (!url) throw new Error('WTT media upload returned no URL');
156
+ const title = String(argv.title || '').trim() || path.basename(target.path);
157
+ const content = `[file:${title}](${url})`;
158
+ const published = await wtt.publish(topicId, content, argv.semanticType || 'TASK_ARTIFACT', {
159
+ type: 'generated_file',
160
+ source: 'agent_upload',
161
+ source_id: topicId,
162
+ file_name: title,
163
+ local_path: target.path,
164
+ size: asset.size || stat.size,
165
+ packaged: files.length > 1 || Boolean(argv.zip),
166
+ original_files: target.originalFiles?.map((file) => path.basename(file)) || undefined,
167
+ });
168
+ console.log(JSON.stringify({ ok: true, topic_id: topicId, file: target.path, url, message: published }, null, 2));
169
+ } finally {
170
+ await wtt.close();
171
+ }
172
+ }
173
+
174
+ function packageUploadFiles(files, config, argv) {
175
+ if (argv.noZip && files.length === 1) return { path: files[0], originalFiles: files };
176
+ const dir = config.artifactDir || path.join(config.workDir || process.cwd(), '.wtt-connect', 'artifacts');
177
+ fs.mkdirSync(dir, { recursive: true });
178
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
179
+ const title = String(argv.title || 'agent-files').replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64) || 'agent-files';
180
+ const zipPath = path.join(dir, `${title}-${stamp}.zip`);
181
+ const result = spawnSync('zip', ['-j', '-q', zipPath, ...files], { encoding: 'utf8' });
182
+ if (result.status !== 0) {
183
+ const detail = result.stderr || result.stdout || `exit ${result.status}`;
184
+ throw new Error(`failed to package files with zip: ${detail}`);
185
+ }
186
+ return { path: zipPath, originalFiles: files };
187
+ }
188
+
189
+ function resolveUploadFilePath(filePath, config) {
190
+ const raw = String(filePath || '').trim();
191
+ if (!raw) throw new Error('empty file path');
192
+ const expanded = raw.startsWith('~/') ? path.join(process.env.HOME || '', raw.slice(2)) : raw;
193
+ const base = path.resolve(config.workDir || process.cwd());
194
+ const resolved = path.resolve(path.isAbsolute(expanded) ? expanded : path.join(base, expanded));
195
+ const roots = [
196
+ config.workDir,
197
+ config.artifactDir,
198
+ config.storeFile ? path.dirname(config.storeFile) : '',
199
+ config.inboxDir,
200
+ ].filter(Boolean).map((root) => path.resolve(root));
201
+ if (!roots.some((root) => isPathInside(resolved, root))) {
202
+ throw new Error(`upload-file refused path outside WTT workspace/state roots: ${resolved}`);
203
+ }
204
+ return resolved;
205
+ }
206
+
207
+ function isPathInside(candidate, root) {
208
+ const rel = path.relative(root, candidate);
209
+ return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
210
+ }
211
+
121
212
  async function uploadArtifact(config, argv) {
122
213
  const dir = argv.dir || argv._[0];
123
214
  if (!dir) throw new Error('upload-artifact requires --dir <path>');
package/src/runner.js CHANGED
@@ -1,7 +1,9 @@
1
1
  import { WTTClient } from './wtt-client.js';
2
+ import { execFile } from 'node:child_process';
2
3
  import fs from 'node:fs';
3
4
  import fsp from 'node:fs/promises';
4
5
  import path from 'node:path';
6
+ import { promisify } from 'node:util';
5
7
  import { WTTApi } from './wtt-api.js';
6
8
  import { SessionManager } from './session-manager.js';
7
9
  import { TTSManager } from './tts.js';
@@ -16,12 +18,14 @@ import { log } from './logger.js';
16
18
  import { buildRuntimeInfo } from './runtime-info.js';
17
19
  import { runShellCommand } from './shell-runner.js';
18
20
  import { TerminalSessionManager } from './terminal-session.js';
19
- import { buildOpenDesignPrompt, buildOpenDesignRepairPrompt, chooseOpenDesignSkills, evaluateOpenDesignArtifact, prepareOpenDesignDir, shouldRenderOpenDesignArtifact } from './opendesign.js';
20
21
 
21
22
  const TERMINAL_STATUSES = new Set(['review', 'done', 'approved', 'cancelled']);
22
- const GENERATED_FILE_EXTENSIONS = new Set(['.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx', '.pdf', '.csv', '.md', '.txt', '.zip']);
23
- const AUTO_DETECTED_FILE_EXTENSIONS = '(?:docx?|pptx?|xlsx?|pdf|csv|zip)';
23
+ const GENERATED_FILE_EXTENSIONS = new Set([
24
+ '.doc', '.docx', '.ppt', '.pptx', '.xls', '.xlsx', '.pdf', '.csv', '.md', '.txt', '.zip',
25
+ '.html', '.htm', '.png', '.jpg', '.jpeg', '.webp', '.mp4', '.webm', '.mov', '.m4v',
26
+ ]);
24
27
  const BROADCAST_MENTIONS = new Set(['all', 'everyone', '全体', '所有人']);
28
+ const execFileAsync = promisify(execFile);
25
29
 
26
30
  export class Runner {
27
31
  constructor(config) {
@@ -188,7 +192,7 @@ export class Runner {
188
192
  '',
189
193
  staged.promptBlock,
190
194
  renderTranscriptBlock(transcripts),
191
- renderGeneratedFileArtifactInstruction(this.config),
195
+ renderGeneratedFileArtifactInstruction(this.config, topicId),
192
196
  'Reply naturally and concisely unless the user asks for detail.',
193
197
  ].filter(Boolean).join('\n');
194
198
  const output = await adapter.run(prompt, {
@@ -203,7 +207,6 @@ export class Runner {
203
207
  await this.maybeAttachSpeech(topicId, reply, `chat-${topicId}`);
204
208
  await this.wtt.publish(topicId, reply, 'CHAT_REPLY');
205
209
  await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'chat', sourceId: topicId, adapter: adapter.name });
206
- await this.maybeRenderOpenDesignArtifact(m, reply, adapter);
207
210
  log('info', 'chat replied', { topicId, chars: reply.length });
208
211
  } catch (err) {
209
212
  await this.wtt.publish(topicId, `执行失败:${err.message}`, 'CHAT_REPLY');
@@ -286,73 +289,11 @@ export class Runner {
286
289
  }
287
290
  }
288
291
 
289
- async maybeRenderOpenDesignArtifact(message, reply, adapter) {
290
- const topicId = String(message.topic_id || '');
291
- if (!topicId || !shouldRenderOpenDesignArtifact(message, reply)) return;
292
- try {
293
- await this.wtt.typing(topicId, 'start', {
294
- statusText: 'OpenDesign 正在生成可视化 artifact',
295
- statusKind: 'opendesign',
296
- adapter: adapter.name,
297
- ttlMs: 90000,
298
- });
299
- const outputDir = await prepareOpenDesignDir(this.config, topicId);
300
- const skills = chooseOpenDesignSkills(message, reply);
301
- const locale = inferLocale(`${message.content || ''}\n${reply || ''}`);
302
- const prompt = await buildOpenDesignPrompt({
303
- config: this.config,
304
- outputDir,
305
- userMessage: message.content || '',
306
- reply,
307
- topicId,
308
- locale,
309
- skills,
310
- });
311
- await adapter.run(prompt, {
312
- sessionKey: `wtt:opendesign:${topicId}`,
313
- topicId,
314
- onProgress: (event) => this.maybePublishProgress(topicId, event, adapter.name),
315
- });
316
- let quality = await evaluateOpenDesignArtifact(outputDir);
317
- if (!quality.ok) {
318
- log('warn', 'opendesign artifact quality check failed; requesting rewrite', { topicId, score: quality.score, issues: quality.issues.join('; ') });
319
- await this.wtt.typing(topicId, 'start', {
320
- statusText: 'OpenDesign 正在重写低质量 artifact',
321
- statusKind: 'opendesign_repair',
322
- adapter: adapter.name,
323
- ttlMs: 120000,
324
- });
325
- await adapter.run(buildOpenDesignRepairPrompt({ outputDir, issues: quality.issues, locale }), {
326
- sessionKey: `wtt:opendesign:${topicId}`,
327
- topicId,
328
- onProgress: (event) => this.maybePublishProgress(topicId, event, adapter.name),
329
- });
330
- quality = await evaluateOpenDesignArtifact(outputDir);
331
- log(quality.ok ? 'info' : 'warn', 'opendesign artifact quality check after rewrite', { topicId, score: quality.score, issues: quality.issues.join('; ') });
332
- }
333
- const artifact = await this.artifacts.uploadDirectory(outputDir, {
334
- source: messageTopicType(message) || 'feed',
335
- sourceId: topicId,
336
- title: `OpenDesign · ${topicId.slice(0, 8)}`,
337
- entry: 'index.html',
338
- type: 'opendesign',
339
- metadata: { message_id: message.id || message.message_id || '', generated_by: adapter.name, opendesign_skills: skills },
340
- });
341
- const previewUrl = webPreviewUrl(artifact.preview_url || artifact.url);
342
- if (previewUrl) {
343
- const title = artifact.title || 'OpenDesign artifact';
344
- await this.wtt.publish(topicId, `[opendesign:${title}](${previewUrl})`, 'TASK_ARTIFACT');
345
- }
346
- log('info', 'opendesign artifact completed', { topicId, preview: previewUrl });
347
- } catch (err) {
348
- log('warn', 'opendesign artifact failed', { topicId, error: err.message });
349
- }
350
- }
351
-
352
292
  async publishGeneratedFileArtifacts(topicId, refs, context = {}) {
353
293
  if (!topicId || !Array.isArray(refs) || !refs.length) return [];
354
294
  const uploaded = [];
355
295
  const seen = new Set();
296
+ const candidates = [];
356
297
  for (const ref of refs) {
357
298
  const resolved = resolveGeneratedFilePath(ref.path, this.config);
358
299
  if (!resolved || seen.has(resolved)) continue;
@@ -360,16 +301,26 @@ export class Runner {
360
301
  try {
361
302
  const stat = await fsp.stat(resolved);
362
303
  if (!stat.isFile()) continue;
304
+ candidates.push({ path: resolved, title: ref.title || path.basename(resolved), stat });
305
+ } catch (err) {
306
+ log('warn', 'generated file artifact skipped', { topicId, file: ref.path, error: err.message });
307
+ }
308
+ }
309
+
310
+ const publishTargets = await packageGeneratedFilesIfNeeded(candidates, this.config, context);
311
+ for (const ref of publishTargets) {
312
+ try {
363
313
  await this.wtt.typing(topicId, 'start', {
364
- statusText: `Agent 正在上传生成文件:${path.basename(resolved)}`,
314
+ statusText: `Agent 正在上传生成文件:${path.basename(ref.path)}`,
365
315
  statusKind: 'artifact_upload',
366
316
  adapter: context.adapter,
367
317
  ttlMs: 60000,
368
318
  });
369
- const asset = await this.artifacts.uploadFile(resolved, { force: true });
319
+ const asset = await this.artifacts.uploadFile(ref.path, { force: true });
370
320
  const url = asset?.url || asset?.public_url;
371
321
  if (!url) continue;
372
- const fileName = ref.title || path.basename(resolved);
322
+ const stat = ref.stat || await fsp.stat(ref.path);
323
+ const fileName = ref.title || path.basename(ref.path);
373
324
  await this.wtt.publish(
374
325
  topicId,
375
326
  `[file:${fileName}](${url})`,
@@ -379,12 +330,14 @@ export class Runner {
379
330
  source: context.source || 'agent',
380
331
  source_id: context.sourceId || '',
381
332
  file_name: fileName,
382
- local_path: resolved,
333
+ local_path: ref.path,
383
334
  mime_type: asset.mime_type || '',
384
335
  size: asset.size || stat.size,
336
+ packaged: Boolean(ref.packaged),
337
+ original_files: ref.originalFiles || undefined,
385
338
  },
386
339
  );
387
- uploaded.push({ path: resolved, asset });
340
+ uploaded.push({ path: ref.path, asset });
388
341
  } catch (err) {
389
342
  log('warn', 'generated file artifact upload failed', { topicId, file: ref.path, error: err.message });
390
343
  }
@@ -442,28 +395,44 @@ function adapterDisplayName(name) {
442
395
  return name || 'Agent';
443
396
  }
444
397
 
445
- function inferLocale(text) {
446
- return /[\u4e00-\u9fff]/.test(String(text || '')) ? 'zh' : 'en';
447
- }
448
-
449
- function webPreviewUrl(url) {
450
- const text = String(url || '').trim();
451
- if (!text) return '';
452
- if (text.startsWith('/api/wtt/')) return text;
453
- if (text.startsWith('/artifacts/')) return `/api/wtt${text}`;
454
- return text;
455
- }
456
-
457
- function renderGeneratedFileArtifactInstruction(config) {
398
+ function renderGeneratedFileArtifactInstruction(config, topicId = '') {
399
+ const workDir = config.workDir || process.cwd();
400
+ const uploadCommand = `wtt-connect upload-file --topic-id ${topicId || '<topic_id>'} --title "optional display name" <file-or-files>`;
458
401
  return [
459
402
  'Generated file artifact rule, for internal execution only:',
460
- `- If you create a user-facing file (.docx, .pptx, .xlsx, .pdf, .csv, .zip), save it under the workspace or artifact directory: ${config.artifactDir}.`,
461
- '- Include one marker line per generated file in your final response, exactly like: [WTT_ARTIFACT_FILE path="relative/or/absolute/file.docx" title="optional display name"].',
403
+ `- If you create a user-facing file (.docx, .pptx, .xlsx, .pdf, .csv, .md, .zip, images, or video), save it under your current WTT agent workspace: ${workDir}.`,
404
+ `- After creating the file, immediately publish it to this WTT conversation by running: ${uploadCommand}`,
405
+ '- For multiple related files, pass all file paths to one upload-file command; wtt-connect will package them into one zip before publishing.',
406
+ '- If upload-file is unavailable, include one marker line per generated file in your final response, exactly like: [WTT_ARTIFACT_FILE path="relative/or/absolute/file.docx" title="optional display name"].',
462
407
  '- Do not use this marker for source code files, temp files, private logs, credentials, or internal scratch files.',
463
- '- wtt-connect will remove the marker from the visible chat and publish a WTT file card.',
408
+ '- wtt-connect will not scan your workspace automatically; only explicit upload-file commands or marker lines are published as WTT file cards.',
464
409
  ].join('\n');
465
410
  }
466
411
 
412
+ async function packageGeneratedFilesIfNeeded(files, config, context = {}) {
413
+ if (!files.length) return [];
414
+ if (files.length === 1) return files;
415
+ const dir = config.artifactDir || path.join(config.workDir || process.cwd(), '.wtt-connect', 'artifacts');
416
+ await fsp.mkdir(dir, { recursive: true });
417
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
418
+ const sourceId = String(context.sourceId || 'chat').replace(/[^a-zA-Z0-9._-]+/g, '-').slice(0, 48) || 'chat';
419
+ const zipPath = path.join(dir, `wtt-generated-${sourceId}-${stamp}.zip`);
420
+ try {
421
+ await execFileAsync('zip', ['-j', '-q', zipPath, ...files.map((file) => file.path)], { timeout: 120000 });
422
+ const stat = await fsp.stat(zipPath);
423
+ return [{
424
+ path: zipPath,
425
+ title: `agent-files-${sourceId}.zip`,
426
+ stat,
427
+ packaged: true,
428
+ originalFiles: files.map((file) => path.basename(file.path)),
429
+ }];
430
+ } catch (err) {
431
+ log('warn', 'generated files zip packaging failed; uploading files individually', { error: err.message, files: files.length });
432
+ return files;
433
+ }
434
+ }
435
+
467
436
  function prepareGeneratedFileArtifacts(text) {
468
437
  const input = String(text || '');
469
438
  const refs = [];
@@ -477,7 +446,6 @@ function prepareGeneratedFileArtifacts(text) {
477
446
  if (parsed.path) refs.push(parsed);
478
447
  return '';
479
448
  });
480
- for (const ref of extractLocalFilePathRefs(cleaned)) refs.push(ref);
481
449
  return { text: cleaned.replace(/\n{3,}/g, '\n\n').trim(), refs: dedupeArtifactRefs(refs) };
482
450
  }
483
451
 
@@ -495,17 +463,6 @@ function parseArtifactLine(body) {
495
463
  return { path: stripPathPunctuation(parts[0] || ''), title: String(parts[1] || '').trim() };
496
464
  }
497
465
 
498
- function extractLocalFilePathRefs(text) {
499
- const refs = [];
500
- const body = String(text || '');
501
- const ext = AUTO_DETECTED_FILE_EXTENSIONS;
502
- const pathLike = new RegExp(`(?:^|[\\s"'\\\`((])((?:~\\/|\\.{1,2}\\/|\\/)[^\\s"'\\\`<>]+?\\.${ext})(?=$|[\\s"'\\\`)).,;,。;])`, 'gi');
503
- for (const m of body.matchAll(pathLike)) refs.push({ path: stripPathPunctuation(m[1]), title: '' });
504
- const basename = new RegExp(`\\b([A-Za-z0-9][A-Za-z0-9._-]{0,160}\\.${ext})\\b`, 'gi');
505
- for (const m of body.matchAll(basename)) refs.push({ path: stripPathPunctuation(m[1]), title: '' });
506
- return refs;
507
- }
508
-
509
466
  function stripPathPunctuation(value) {
510
467
  return String(value || '').trim().replace(/^["'`]+|["'`.,;:,。;:))\]]+$/g, '');
511
468
  }
@@ -532,7 +489,10 @@ function resolveGeneratedFilePath(filePath, config) {
532
489
 
533
490
  const expanded = raw.startsWith('~/') ? path.join(process.env.HOME || '', raw.slice(2)) : raw;
534
491
  const base = path.resolve(config.workDir || process.cwd());
535
- const resolved = path.resolve(path.isAbsolute(expanded) ? expanded : path.join(base, expanded));
492
+ const relativeRoots = [base, config.artifactDir].filter(Boolean).map((root) => path.resolve(root));
493
+ const resolved = path.resolve(path.isAbsolute(expanded)
494
+ ? expanded
495
+ : (relativeRoots.map((root) => path.join(root, expanded)).find((candidate) => fs.existsSync(candidate)) || path.join(base, expanded)));
536
496
  const roots = [
537
497
  config.workDir,
538
498
  config.artifactDir,
@@ -859,7 +819,7 @@ function buildTaskPrompt(task, config, staged = { promptBlock: '' }, transcripts
859
819
  renderTranscriptBlock(transcripts),
860
820
  '',
861
821
  config.requireCommitPush ? 'For code changes, commit and push before final response, and include commit id.' : '',
862
- renderGeneratedFileArtifactInstruction(config),
822
+ renderGeneratedFileArtifactInstruction(config, task.topic_id || task.topicId || ''),
863
823
  'Return a concise final summary with evidence, changed files, tests, artifacts, and blockers.',
864
824
  ].filter(Boolean).join('\n');
865
825
  }
package/src/wtt-client.js CHANGED
@@ -59,6 +59,31 @@ export class WTTClient {
59
59
  }
60
60
  }
61
61
 
62
+ async connectForActions(timeoutMs = 15000) {
63
+ if (!globalThis.WebSocket) throw new Error('Node.js global WebSocket is unavailable; use Node >=22');
64
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) return;
65
+ log('info', 'connecting WTT websocket for action', { url: this.config.wttWsUrl, agentId: this.config.agentId });
66
+ const ws = new WebSocket(this.config.wttWsUrl);
67
+ this.ws = ws;
68
+ await onceOpen(ws, timeoutMs);
69
+ ws.addEventListener('message', (event) => this.handleMessage(String(event.data)).catch((err) => {
70
+ log('warn', 'WTT websocket action message failed', { error: err?.message || err });
71
+ }));
72
+ ws.addEventListener('close', () => {
73
+ if (this.ws === ws) this.ws = null;
74
+ for (const [, p] of this.pending) p.reject(new Error('WTT websocket closed'));
75
+ this.pending.clear();
76
+ });
77
+ if (this.config.token) {
78
+ try {
79
+ await this.action('auth', { token: this.config.token }, timeoutMs);
80
+ } catch (err) {
81
+ log('warn', 'WTT websocket action auth failed; continuing for agent-scoped actions', { error: err?.message || err });
82
+ }
83
+ }
84
+ await this.sendHeartbeat();
85
+ }
86
+
62
87
  async close() {
63
88
  this.closed = true;
64
89
  if (this.ws) this.ws.close();
@@ -0,0 +1,14 @@
1
+ [Unit]
2
+ Description=WTT Connect Claude Code Agent
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=/mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect
9
+ ExecStart=/usr/bin/node --experimental-websocket /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/bin/wtt-connect.js start --env-file /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/.env.claude
10
+ Restart=always
11
+ RestartSec=5
12
+
13
+ [Install]
14
+ WantedBy=default.target
@@ -0,0 +1,14 @@
1
+ [Unit]
2
+ Description=WTT Connect Codex Agent
3
+ After=network-online.target
4
+ Wants=network-online.target
5
+
6
+ [Service]
7
+ Type=simple
8
+ WorkingDirectory=/mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect
9
+ ExecStart=/usr/bin/node --experimental-websocket /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/bin/wtt-connect.js start --env-file /mnt/wd10t/saiph/wtt/wtt/tools/wtt-connect/.env
10
+ Restart=always
11
+ RestartSec=5
12
+
13
+ [Install]
14
+ WantedBy=default.target