wtt-connect 0.2.6 → 0.2.8

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
@@ -28,6 +28,7 @@ Implemented production-oriented surfaces:
28
28
  - HTTP task status update path
29
29
  - Permission broker for Codex modes, including explicit opt-in for dangerous `yolo`
30
30
  - Optional WTT media artifact upload path (`/media/sign` → direct upload → `/media/commit`)
31
+ - 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
31
32
  - 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)
32
33
  - STT extension point (`command` and OpenAI/Whisper providers) for audio attachments
33
34
  - Setup/claim-code command, smoke-task/smoke-chat commands, start script, and macOS launchd installer
@@ -106,6 +107,35 @@ A JSON config may also be supplied:
106
107
  node ./bin/wtt-connect.js start --config ./config.json
107
108
  ```
108
109
 
110
+ ### Generated files in feed chat
111
+
112
+ Codex, Claude Code, and other adapters can generate user-facing files on the agent host and send them back to WTT feed chat as clickable file cards.
113
+
114
+ Supported generated artifact types:
115
+
116
+ ```text
117
+ .doc .docx .ppt .pptx .xls .xlsx .pdf .csv .md .txt .zip
118
+ ```
119
+
120
+ 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:
121
+
122
+ ```text
123
+ [WTT_ARTIFACT_FILE path="report.docx" title="Project Report"]
124
+ [WTT_ARTIFACT_FILE path="/absolute/workspace/slides.pptx" title="Review Slides"]
125
+ ```
126
+
127
+ `wtt-connect` removes those marker lines from the visible reply, uploads each file through WTT media, and publishes file cards like:
128
+
129
+ ```text
130
+ [file:Project Report](https://...)
131
+ ```
132
+
133
+ Notes:
134
+
135
+ - Generated file markers are explicit user-facing uploads; they are uploaded even when `WTT_CONNECT_UPLOAD_ARTIFACTS=0`.
136
+ - Files outside the configured workspace, state directory, inbox directory, or artifact directory are ignored to avoid accidental leakage.
137
+ - `WTT_CONNECT_UPLOAD_ARTIFACTS=1` still controls automatic summary/TTS uploads; it is not required for explicit generated file markers.
138
+
109
139
  ### One-command service binding
110
140
 
111
141
  The recommended install path for users is npm + `up`:
@@ -265,6 +295,8 @@ Uninstall one identity with:
265
295
 
266
296
  Discussion/collaborative topics are mention-gated: `wtt-connect` only replies when the message targets its `WTT_AGENT_ID`, one of `WTT_CONNECT_AGENT_ALIASES`, backend-resolved `runner_agent_id`, or a multi-mention `mention_target_agent_ids` entry. P2P and task topics still run normally.
267
297
 
298
+ In discussion/collaborative topics, `wtt-connect` also injects a silent collaboration standard into each agent prompt. Agents should treat the topic as a shared workboard, preserve shared state, split non-trivial work into owned tasks, state concrete done criteria, report execution evidence, challenge weak assumptions, and only @mention the next specific agent when handoff is required.
299
+
268
300
  Adapter routing can be explicit through task fields such as `exec_mode=gemini`, or through message mentions such as `@codex`, `@claude`, `@cursor`, `@gemini`, `@qoder`, `@opencode`, `@iflow`, `@kimi`, `@pi`, `@acp`, or `@devin` when those adapters are enabled.
269
301
 
270
302
  OpenClaw is deliberately not part of the default `wtt-connect` adapter set because WTT already has the first-class `wtt-plugin` for OpenClaw.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtt-connect",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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/artifacts.js CHANGED
@@ -17,8 +17,8 @@ export class ArtifactManager {
17
17
  return file;
18
18
  }
19
19
 
20
- async uploadFile(filePath) {
21
- if (!this.config.uploadArtifacts) return null;
20
+ async uploadFile(filePath, options = {}) {
21
+ if (!this.config.uploadArtifacts && !options.force) return null;
22
22
  const stat = await fs.stat(filePath);
23
23
  if (!stat.isFile()) return null;
24
24
  const mimeType = lookupMime(filePath);
package/src/mime.js CHANGED
@@ -1,6 +1,10 @@
1
1
  const MAP = new Map([
2
2
  ['.txt', 'text/plain'], ['.md', 'text/markdown'], ['.json', 'application/json'],
3
- ['.html', 'text/html'], ['.pdf', 'application/pdf'], ['.zip', 'application/zip'],
3
+ ['.html', 'text/html'], ['.htm', 'text/html'], ['.pdf', 'application/pdf'], ['.zip', 'application/zip'],
4
+ ['.csv', 'text/csv'], ['.xml', 'text/xml'], ['.yml', 'application/x-yaml'], ['.yaml', 'application/x-yaml'],
5
+ ['.doc', 'application/msword'], ['.docx', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'],
6
+ ['.ppt', 'application/vnd.ms-powerpoint'], ['.pptx', 'application/vnd.openxmlformats-officedocument.presentationml.presentation'],
7
+ ['.xls', 'application/vnd.ms-excel'], ['.xlsx', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'],
4
8
  ['.png', 'image/png'], ['.jpg', 'image/jpeg'], ['.jpeg', 'image/jpeg'],
5
9
  ['.gif', 'image/gif'], ['.webp', 'image/webp'], ['.mp3', 'audio/mpeg'],
6
10
  ['.wav', 'audio/wav'], ['.ogg', 'audio/ogg'], ['.webm', 'audio/webm'], ['.mp4', 'video/mp4'], ['.mov', 'video/quicktime'],
package/src/runner.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { WTTClient } from './wtt-client.js';
2
2
  import fs from 'node:fs';
3
+ import fsp from 'node:fs/promises';
4
+ import path from 'node:path';
3
5
  import { WTTApi } from './wtt-api.js';
4
6
  import { SessionManager } from './session-manager.js';
5
7
  import { TTSManager } from './tts.js';
@@ -17,6 +19,8 @@ import { TerminalSessionManager } from './terminal-session.js';
17
19
  import { buildOpenDesignPrompt, buildOpenDesignRepairPrompt, chooseOpenDesignSkills, evaluateOpenDesignArtifact, prepareOpenDesignDir, shouldRenderOpenDesignArtifact } from './opendesign.js';
18
20
 
19
21
  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)';
20
24
 
21
25
  export class Runner {
22
26
  constructor(config) {
@@ -183,6 +187,7 @@ export class Runner {
183
187
  '',
184
188
  staged.promptBlock,
185
189
  renderTranscriptBlock(transcripts),
190
+ renderGeneratedFileArtifactInstruction(this.config),
186
191
  'Reply naturally and concisely unless the user asks for detail.',
187
192
  ].filter(Boolean).join('\n');
188
193
  const output = await adapter.run(prompt, {
@@ -192,9 +197,11 @@ export class Runner {
192
197
  images: staged.images,
193
198
  onProgress: (event) => this.maybePublishProgress(topicId, event, adapter.name),
194
199
  });
195
- const reply = stripHiddenContextLeak(output || '(empty response)') || '(empty response)';
200
+ const prepared = prepareGeneratedFileArtifacts(stripHiddenContextLeak(output || '(empty response)') || '(empty response)');
201
+ const reply = prepared.text || '(empty response)';
196
202
  await this.maybeAttachSpeech(topicId, reply, `chat-${topicId}`);
197
203
  await this.wtt.publish(topicId, reply, 'CHAT_REPLY');
204
+ await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'chat', sourceId: topicId, adapter: adapter.name });
198
205
  await this.maybeRenderOpenDesignArtifact(m, reply, adapter);
199
206
  log('info', 'chat replied', { topicId, chars: reply.length });
200
207
  } catch (err) {
@@ -226,7 +233,8 @@ export class Runner {
226
233
  images: staged.images,
227
234
  onProgress: (ev) => topicId ? this.maybePublishProgress(topicId, ev, adapter.name) : Promise.resolve(),
228
235
  });
229
- const summary = output || '(empty response)';
236
+ const prepared = prepareGeneratedFileArtifacts(output || '(empty response)');
237
+ const summary = prepared.text || '(empty response)';
230
238
  await this.maybeRelayArenaOpenCLResult(task, summary);
231
239
  const artifact = await this.materializeTaskArtifact(taskId, summary);
232
240
  const commitIds = extractCommitIds(summary);
@@ -235,6 +243,7 @@ export class Runner {
235
243
  await this.api.patchTask(taskId, patch);
236
244
  if (topicId) {
237
245
  await this.wtt.publish(topicId, summary, 'TASK_SUMMARY');
246
+ await this.publishGeneratedFileArtifacts(topicId, prepared.refs, { source: 'task', sourceId: taskId, adapter: adapter.name });
238
247
  if (artifact?.asset?.url) await this.wtt.publish(topicId, `Artifact: ${artifact.asset.url}`, 'TASK_ARTIFACT');
239
248
  }
240
249
  log('info', 'task completed', { taskId, chars: summary.length });
@@ -339,6 +348,49 @@ export class Runner {
339
348
  }
340
349
  }
341
350
 
351
+ async publishGeneratedFileArtifacts(topicId, refs, context = {}) {
352
+ if (!topicId || !Array.isArray(refs) || !refs.length) return [];
353
+ const uploaded = [];
354
+ const seen = new Set();
355
+ for (const ref of refs) {
356
+ const resolved = resolveGeneratedFilePath(ref.path, this.config);
357
+ if (!resolved || seen.has(resolved)) continue;
358
+ seen.add(resolved);
359
+ try {
360
+ const stat = await fsp.stat(resolved);
361
+ if (!stat.isFile()) continue;
362
+ await this.wtt.typing(topicId, 'start', {
363
+ statusText: `Agent 正在上传生成文件:${path.basename(resolved)}`,
364
+ statusKind: 'artifact_upload',
365
+ adapter: context.adapter,
366
+ ttlMs: 60000,
367
+ });
368
+ const asset = await this.artifacts.uploadFile(resolved, { force: true });
369
+ const url = asset?.url || asset?.public_url;
370
+ if (!url) continue;
371
+ const fileName = ref.title || path.basename(resolved);
372
+ await this.wtt.publish(
373
+ topicId,
374
+ `[file:${fileName}](${url})`,
375
+ 'TASK_ARTIFACT',
376
+ {
377
+ type: 'generated_file',
378
+ source: context.source || 'agent',
379
+ source_id: context.sourceId || '',
380
+ file_name: fileName,
381
+ local_path: resolved,
382
+ mime_type: asset.mime_type || '',
383
+ size: asset.size || stat.size,
384
+ },
385
+ );
386
+ uploaded.push({ path: resolved, asset });
387
+ } catch (err) {
388
+ log('warn', 'generated file artifact upload failed', { topicId, file: ref.path, error: err.message });
389
+ }
390
+ }
391
+ return uploaded;
392
+ }
393
+
342
394
  async materializeTaskArtifact(taskId, summary) {
343
395
  try {
344
396
  return await this.artifacts.uploadText(`task-${taskId}-summary.md`, summary);
@@ -401,6 +453,100 @@ function webPreviewUrl(url) {
401
453
  return text;
402
454
  }
403
455
 
456
+ function renderGeneratedFileArtifactInstruction(config) {
457
+ return [
458
+ 'Generated file artifact rule, for internal execution only:',
459
+ `- If you create a user-facing file (.docx, .pptx, .xlsx, .pdf, .csv, .zip), save it under the workspace or artifact directory: ${config.artifactDir}.`,
460
+ '- 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"].',
461
+ '- Do not use this marker for source code files, temp files, private logs, credentials, or internal scratch files.',
462
+ '- wtt-connect will remove the marker from the visible chat and publish a WTT file card.',
463
+ ].join('\n');
464
+ }
465
+
466
+ function prepareGeneratedFileArtifacts(text) {
467
+ const input = String(text || '');
468
+ const refs = [];
469
+ let cleaned = input.replace(/^\s*\[WTT_ARTIFACT_FILE\s+([^\]]+)\]\s*$/gim, (_all, attrs) => {
470
+ const parsed = parseArtifactMarkerAttrs(attrs);
471
+ if (parsed.path) refs.push(parsed);
472
+ return '';
473
+ });
474
+ cleaned = cleaned.replace(/^\s*WTT_ARTIFACT_FILE\s*:\s*(.+?)\s*$/gim, (_all, body) => {
475
+ const parsed = parseArtifactLine(body);
476
+ if (parsed.path) refs.push(parsed);
477
+ return '';
478
+ });
479
+ for (const ref of extractLocalFilePathRefs(cleaned)) refs.push(ref);
480
+ return { text: cleaned.replace(/\n{3,}/g, '\n\n').trim(), refs: dedupeArtifactRefs(refs) };
481
+ }
482
+
483
+ function parseArtifactMarkerAttrs(attrs) {
484
+ const out = {};
485
+ for (const m of String(attrs || '').matchAll(/([a-zA-Z_][\w-]*)\s*=\s*(?:"([^"]+)"|'([^']+)'|([^\s]+))/g)) {
486
+ out[m[1]] = m[2] || m[3] || m[4] || '';
487
+ }
488
+ return { path: String(out.path || out.file || '').trim(), title: String(out.title || out.name || '').trim() };
489
+ }
490
+
491
+ function parseArtifactLine(body) {
492
+ const text = String(body || '').trim();
493
+ const parts = text.split(/\s+\|\s+/, 2);
494
+ return { path: stripPathPunctuation(parts[0] || ''), title: String(parts[1] || '').trim() };
495
+ }
496
+
497
+ function extractLocalFilePathRefs(text) {
498
+ const refs = [];
499
+ const body = String(text || '');
500
+ const ext = AUTO_DETECTED_FILE_EXTENSIONS;
501
+ const pathLike = new RegExp(`(?:^|[\\s"'\\\`((])((?:~\\/|\\.{1,2}\\/|\\/)[^\\s"'\\\`<>]+?\\.${ext})(?=$|[\\s"'\\\`)).,;,。;])`, 'gi');
502
+ for (const m of body.matchAll(pathLike)) refs.push({ path: stripPathPunctuation(m[1]), title: '' });
503
+ const basename = new RegExp(`\\b([A-Za-z0-9][A-Za-z0-9._-]{0,160}\\.${ext})\\b`, 'gi');
504
+ for (const m of body.matchAll(basename)) refs.push({ path: stripPathPunctuation(m[1]), title: '' });
505
+ return refs;
506
+ }
507
+
508
+ function stripPathPunctuation(value) {
509
+ return String(value || '').trim().replace(/^["'`]+|["'`.,;:,。;:))\]]+$/g, '');
510
+ }
511
+
512
+ function dedupeArtifactRefs(refs) {
513
+ const out = [];
514
+ const seen = new Set();
515
+ for (const ref of refs) {
516
+ const filePath = stripPathPunctuation(ref?.path || '');
517
+ if (!filePath) continue;
518
+ const key = filePath.toLowerCase();
519
+ if (seen.has(key)) continue;
520
+ seen.add(key);
521
+ out.push({ path: filePath, title: String(ref?.title || '').trim() });
522
+ }
523
+ return out;
524
+ }
525
+
526
+ function resolveGeneratedFilePath(filePath, config) {
527
+ const raw = stripPathPunctuation(filePath);
528
+ if (!raw || /^https?:\/\//i.test(raw)) return null;
529
+ const ext = path.extname(raw).toLowerCase();
530
+ if (!GENERATED_FILE_EXTENSIONS.has(ext)) return null;
531
+
532
+ const expanded = raw.startsWith('~/') ? path.join(process.env.HOME || '', raw.slice(2)) : raw;
533
+ const base = path.resolve(config.workDir || process.cwd());
534
+ const resolved = path.resolve(path.isAbsolute(expanded) ? expanded : path.join(base, expanded));
535
+ const roots = [
536
+ config.workDir,
537
+ config.artifactDir,
538
+ config.storeFile ? path.dirname(config.storeFile) : '',
539
+ config.inboxDir,
540
+ ].filter(Boolean).map((root) => path.resolve(root));
541
+ if (!roots.some((root) => isPathInside(resolved, root))) return null;
542
+ return resolved;
543
+ }
544
+
545
+ function isPathInside(candidate, root) {
546
+ const rel = path.relative(root, candidate);
547
+ return rel === '' || (rel && !rel.startsWith('..') && !path.isAbsolute(rel));
548
+ }
549
+
404
550
  function messageTopicType(m) {
405
551
  return String(m.topic_type || metadataValue(m.metadata, 'topic_type') || metadataValue(m.metadata, 'topicType') || '').toLowerCase();
406
552
  }
@@ -427,6 +573,16 @@ function renderDiscussionRoutingInstruction(m, config) {
427
573
  '- If you want a different agent to continue, @mention only that other agent by its exact visible name or agent id from the conversation.',
428
574
  '- If your reply is a final answer, summary, or no further agent action is needed, do not @mention anyone.',
429
575
  '- Do not use @all or vague mentions; mention only the specific next agent that should act.',
576
+ '',
577
+ 'Internal WTT group-collaboration operating standard. Use it silently to structure useful replies:',
578
+ '- Treat the topic as a shared workboard. Preserve shared state: goal, assumptions, decisions, owners, artifacts, blockers, and evidence.',
579
+ '- For non-trivial work, make planning explicit before execution: split the work into small tasks, assign one owner per task, and define a concrete done criterion for each task.',
580
+ '- When you are the owner of a task, report in this shape when useful: `Owner: <your name>`, `Task: <specific work>`, `Done criteria: <observable result>`, `Status: planned|running|blocked|done`, `Evidence: <files/tests/commands/artifacts>`, `Next: <next action or @agent>`.',
581
+ '- Prefer action over discussion: if you can safely execute or verify something in your environment, do it and report evidence instead of only suggesting it.',
582
+ '- Challenge weak assumptions, missing tests, unclear interfaces, and risky plans. Keep criticism concrete and tied to the task outcome.',
583
+ '- Do not repeat another agent unless adding new evidence, a correction, a decision, or a next action.',
584
+ '- For simple direct questions, answer normally and briefly; do not force the full task template.',
585
+ '- For final summaries, include completed work, remaining blockers, and recommended next owner only if more work is needed.',
430
586
  ].join('\n');
431
587
  }
432
588
 
@@ -696,6 +852,7 @@ function buildTaskPrompt(task, config, staged = { promptBlock: '' }, transcripts
696
852
  renderTranscriptBlock(transcripts),
697
853
  '',
698
854
  config.requireCommitPush ? 'For code changes, commit and push before final response, and include commit id.' : '',
855
+ renderGeneratedFileArtifactInstruction(config),
699
856
  'Return a concise final summary with evidence, changed files, tests, artifacts, and blockers.',
700
857
  ].filter(Boolean).join('\n');
701
858
  }