wtt-connect 0.2.10 → 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,12 +21,11 @@ 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
28
27
  - HTTP task status update path
29
- - Permission broker for Codex modes, including explicit opt-in for dangerous `yolo`
28
+ - Permission broker for agent modes; `full-auto` is the default no-approval mode for Codex and Claude Code
30
29
  - Optional WTT media artifact upload path (`/media/sign` → direct upload → `/media/commit`)
31
30
  - Agent-generated file artifacts for WTT feed chat (`.docx`, `.pptx`, `.xlsx`, `.pdf`, `.csv`, `.zip`) using explicit final-response markers that are converted into WTT file cards
32
31
  - Attachment staging for WTT message media/files; image attachments are passed to Codex with `--image` (including Claude Code image fallback when the Claude provider cannot view images directly)
@@ -90,6 +89,7 @@ Important settings:
90
89
  - `WTT_CONNECT_ADAPTERS=codex,claude-code,cursor,gemini,qoder,opencode,iflow,kimi,pi,acp,devin`
91
90
  - `WTT_CONNECT_WORKDIR`
92
91
  - `WTT_CONNECT_MODE=full-auto|auto-edit|yolo|suggest`
92
+ - `full-auto` is the default no-approval mode: Codex runs with `--dangerously-bypass-approvals-and-sandbox`; Claude Code runs with `--dangerously-skip-permissions --permission-mode bypassPermissions`
93
93
  - `WTT_CONNECT_AGENT_ALIASES=alias1,alias2` for extra @mention names in discussion/collaborative topics; `WTT_AGENT_ID` always works
94
94
  - `WTT_CONNECT_ALLOW_YOLO=1` only when intentionally using `yolo`
95
95
  - `WTT_CONNECT_STATE_DIR` / `WTT_CONNECT_STORE_FILE` for durable sessions
@@ -121,10 +121,19 @@ Codex, Claude Code, and other adapters can generate user-facing files on the age
121
121
  Supported generated artifact types:
122
122
 
123
123
  ```text
124
- .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
125
125
  ```
126
126
 
127
- 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:
128
137
 
129
138
  ```text
130
139
  [WTT_ARTIFACT_FILE path="report.docx" title="Project Report"]
@@ -139,7 +148,7 @@ When an agent creates a deliverable file, it should save the file under its work
139
148
 
140
149
  Notes:
141
150
 
142
- - 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`.
143
152
  - Files outside the configured workspace, state directory, inbox directory, or artifact directory are ignored to avoid accidental leakage.
144
153
  - `WTT_CONNECT_UPLOAD_ARTIFACTS=1` still controls automatic summary/TTS uploads; it is not required for explicit generated file markers.
145
154
 
@@ -267,7 +276,7 @@ wtt-connect up codex agent-codex '***'
267
276
  wtt-connect up claude-code agent-claude '***'
268
277
  ```
269
278
 
270
- Use named options when you need a custom service name, permission mode, or boot-before-login behavior:
279
+ Use named options when you need a custom service name, a stricter permission mode, or boot-before-login behavior:
271
280
 
272
281
  ```bash
273
282
  ./scripts/install-systemd-user.sh \
@@ -275,8 +284,7 @@ Use named options when you need a custom service name, permission mode, or boot-
275
284
  --agent-id agent-codex \
276
285
  --token '***' \
277
286
  --adapter codex \
278
- --mode yolo \
279
- --allow-yolo \
287
+ --mode full-auto \
280
288
  --publish-progress \
281
289
  --enable-linger
282
290
 
@@ -285,8 +293,7 @@ Use named options when you need a custom service name, permission mode, or boot-
285
293
  --agent-id agent-claude \
286
294
  --token '***' \
287
295
  --adapter claude-code \
288
- --mode yolo \
289
- --allow-yolo \
296
+ --mode full-auto \
290
297
  --publish-progress \
291
298
  --enable-linger
292
299
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtt-connect",
3
- "version": "0.2.10",
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",
@@ -42,7 +42,7 @@ export class ClaudeCodeAdapter {
42
42
  const args = [];
43
43
  if (sessionId) args.push('--resume', sessionId);
44
44
  args.push('-p', prompt, '--output-format', 'stream-json', '--verbose');
45
- if (this.config.mode === 'yolo') args.push('--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions');
45
+ if (['full-auto', 'yolo'].includes(this.config.mode)) args.push('--dangerously-skip-permissions', '--permission-mode', 'bypassPermissions');
46
46
  if (this.config.model) args.push('--model', this.config.model);
47
47
  return args;
48
48
  }
@@ -118,7 +118,7 @@ function extractSessionId(ev) {
118
118
 
119
119
  async function runCodexVision(bin, prompt, images, cwd, timeoutMs, config = {}, onEvent) {
120
120
  const args = ['exec', '--skip-git-repo-check', '--json', '--cd', cwd];
121
- if (config.mode === 'yolo') args.push('--dangerously-bypass-approvals-and-sandbox');
121
+ if (['full-auto', 'yolo'].includes(config.mode)) args.push('--dangerously-bypass-approvals-and-sandbox');
122
122
  for (const img of images || []) args.push('--image', img);
123
123
  args.push('-');
124
124
  return new Promise((resolve, reject) => {
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>');
@@ -1,7 +1,7 @@
1
1
  const MODE_TO_CODEX = {
2
2
  suggest: [],
3
3
  'auto-edit': ['--full-auto'],
4
- 'full-auto': ['--full-auto'],
4
+ 'full-auto': ['--dangerously-bypass-approvals-and-sandbox'],
5
5
  yolo: ['--dangerously-bypass-approvals-and-sandbox'],
6
6
  };
7
7
 
@@ -23,6 +23,7 @@ export class PermissionBroker {
23
23
 
24
24
  describe() {
25
25
  if (this.config.mode === 'yolo') return 'yolo/dangerously-bypass-approvals-and-sandbox';
26
+ if (this.config.mode === 'full-auto') return 'full-auto/dangerously-bypass-approvals-and-sandbox';
26
27
  return this.config.mode;
27
28
  }
28
29
  }
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