wtt-connect 0.2.1 → 0.2.3

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,6 +21,7 @@ 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
24
25
  - TTS extension point:
25
26
  - `none`
26
27
  - `macos-say` using the macOS `say` command, emitted as WAV when upload is enabled
@@ -142,7 +143,7 @@ Workdir rules:
142
143
 
143
144
  - If `--workdir` or `WTT_CONNECT_WORKDIR` is provided, that exact directory is used for agent execution, shell, terminal, runtime info, and adapter cwd.
144
145
  - If no workdir is provided, `wtt-connect` uses `./workspaces/<agent_id>` under the current default directory. This prevents multiple claimed agents on the same host from sharing the `wtt-connect` package directory.
145
- - For an existing profile, edit `~/.config/wtt-connect/profiles/<profile>.env` and change `WTT_CONNECT_WORKDIR`, then run `wtt-connect restart <profile>`.
146
+ - For an existing profile, run `wtt-connect workdir <profile> /path/to/workspace --restart`. This updates `~/.config/wtt-connect/profiles/<profile>.env`, creates the directory, and restarts the service.
146
147
 
147
148
  Useful flags:
148
149
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wtt-connect",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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
@@ -10,7 +10,7 @@ import { DurableStore } from './store.js';
10
10
  import { log, redact } from './logger.js';
11
11
  import { adapterBin, normalizeProfileName } from './adapters/generic-cli.js';
12
12
  import { normalizeAdapterName } from './adapters/index.js';
13
- import { down, listProfiles, logs, resolveProfileEnvFile, restart, status, up } from './service-manager.js';
13
+ import { down, listProfiles, logs, resolveProfileEnvFile, restart, status, up, workdir } from './service-manager.js';
14
14
 
15
15
  export async function main(args) {
16
16
  const cmd = args[0] || 'help';
@@ -23,6 +23,7 @@ export async function main(args) {
23
23
  if (cmd === 'status') return status(argv);
24
24
  if (cmd === 'restart') return restart(argv);
25
25
  if (cmd === 'logs') return logs(argv);
26
+ if (cmd === 'workdir') return workdir(argv);
26
27
  if (cmd === 'down' || cmd === 'unlink') return down(argv);
27
28
  if (cmd === 'doctor') return doctor(loadConfig(argv));
28
29
  if (cmd === 'setup') return setup(loadConfig(argv), argv);
@@ -61,6 +62,7 @@ function parseArgs(args) {
61
62
  else if (a === '--allow-yolo' || a === '--allow-dangerous-permissions') out.allowYolo = true;
62
63
  else if (a === '--yes' || a === '-y') out.yes = true;
63
64
  else if (a === '--publish-progress') out.publishProgress = true;
65
+ else if (a === '--restart') out.restart = true;
64
66
  else if (a === '--enable-linger') out.enableLinger = true;
65
67
  else if (a === '--no-start') out.noStart = true;
66
68
  else if (a === '--node-bin') out.nodeBin = args[++i];
@@ -97,6 +99,8 @@ Commands:
97
99
  status [profile|all] Show systemd service status for profiles
98
100
  restart [profile|all] Restart one or all configured services
99
101
  logs <profile> [--lines 100] Show service logs
102
+ workdir <profile> [path] [--restart]
103
+ Show or update the agent execution directory
100
104
  down <profile> Stop/disable service; keep profile/state files
101
105
  unlink <profile> Alias for down
102
106
  setup [--claim-code] Register/write local .env and optionally print claim code
package/src/opendesign.js CHANGED
@@ -54,9 +54,12 @@ export async function buildOpenDesignPrompt({ config, outputDir, userMessage, re
54
54
  '- index.html must be complete and runnable in a sandbox iframe.',
55
55
  '- Do not use external CDNs, remote images, external fonts, forms, cookies, localStorage, or network calls.',
56
56
  '- Use inline SVG or local CSS/JS only.',
57
+ '- Treat this as a premium visual explainer, not a documentation page. The first viewport must look intentionally designed.',
57
58
  '- For formula/principle explanations include: a main diagram, step animation, symbol/metric table, minimal concrete example, explanatory text beside visuals, and final compact summary.',
58
59
  '- For UI/prototype requests include realistic fake data, hover states, and meaningful state transitions.',
59
60
  '',
61
+ visualQualityContract(zh),
62
+ '',
60
63
  `WTT topic_id: ${topicId || 'unknown'}`,
61
64
  '',
62
65
  zh ? '用户原始消息:' : 'Original user message:',
@@ -69,6 +72,69 @@ export async function buildOpenDesignPrompt({ config, outputDir, userMessage, re
69
72
  ].join('\n');
70
73
  }
71
74
 
75
+ export function buildOpenDesignRepairPrompt({ outputDir, issues = [], locale = 'zh' }) {
76
+ const zh = locale === 'zh';
77
+ return [
78
+ 'The previous OpenDesign artifact did not meet WTT visual quality requirements.',
79
+ `Rewrite the artifact in place in this directory: ${outputDir}`,
80
+ 'Keep the same answer/topic meaning, but replace weak layout, weak styling, and missing motion.',
81
+ '',
82
+ 'Detected issues:',
83
+ ...issues.map((issue) => `- ${issue}`),
84
+ '',
85
+ visualQualityContract(zh),
86
+ '',
87
+ 'Overwrite index.html, style.css, and main.js as needed. Reply with only: index.html',
88
+ ].join('\n');
89
+ }
90
+
91
+ export async function evaluateOpenDesignArtifact(dir) {
92
+ const root = path.resolve(dir);
93
+ const indexPath = path.join(root, 'index.html');
94
+ const stylePath = path.join(root, 'style.css');
95
+ const scriptPath = path.join(root, 'main.js');
96
+ const issues = [];
97
+ let index = '';
98
+ let style = '';
99
+ let script = '';
100
+ try {
101
+ index = await fs.readFile(indexPath, 'utf8');
102
+ } catch {
103
+ return { ok: false, score: 0, issues: ['missing index.html'] };
104
+ }
105
+ try { style = await fs.readFile(stylePath, 'utf8'); } catch {}
106
+ try { script = await fs.readFile(scriptPath, 'utf8'); } catch {}
107
+ const combined = `${index}\n${style}\n${script}`;
108
+ let score = 0;
109
+
110
+ if (combined.length >= 7000) score += 1;
111
+ else issues.push('artifact is too sparse; expected a dense, high-fidelity explainer');
112
+
113
+ if (/<svg[\s>]/i.test(combined) || /<canvas[\s>]/i.test(combined)) score += 1;
114
+ else issues.push('missing SVG/canvas diagram for visual explanation');
115
+
116
+ if (/@keyframes|animation(?:-name)?:|transition:|stroke-dasharray|requestAnimationFrame/i.test(combined)) score += 1;
117
+ else issues.push('missing meaningful animation or motion system');
118
+
119
+ if (/:root\s*\{|--[a-z0-9-]+:/i.test(combined)) score += 1;
120
+ else issues.push('missing explicit design tokens / CSS variables');
121
+
122
+ const structureCount = (combined.match(/<(section|article|aside|figure|table)\b/gi) || []).length;
123
+ if (structureCount >= 5) score += 1;
124
+ else issues.push('layout lacks enough structured sections/cards/tables');
125
+
126
+ if (/示例|例子|example|case|sample/i.test(combined)) score += 1;
127
+ else issues.push('missing concrete example section');
128
+
129
+ if (/公式|formula|symbol|符号|metric|指标|table|thead|tbody/i.test(combined)) score += 1;
130
+ else issues.push('missing formula/symbol/metric explanation');
131
+
132
+ if (/data-step|step-|timeline|progress|播放|replay|next/i.test(combined)) score += 1;
133
+ else issues.push('missing step-by-step interaction/state');
134
+
135
+ return { ok: score >= 6, score, issues };
136
+ }
137
+
72
138
  async function loadSkillBlocks(config, skillNames) {
73
139
  const root = config.openDesignSkillsDir || '';
74
140
  const blocks = [];
@@ -93,6 +159,29 @@ function fallbackSkillBlock() {
93
159
  ].join('\n');
94
160
  }
95
161
 
162
+ function visualQualityContract(zh) {
163
+ return [
164
+ 'WTT OpenDesign visual quality contract:',
165
+ '- Canvas: design for a 1440x900 whiteboard/preview area, responsive down to tablet. Use full-width composition, not a narrow article.',
166
+ '- Visual hierarchy: strong title block, one dominant SVG/canvas diagram, one compact example panel, one formula/symbol table, one step timeline, and a final takeaway strip.',
167
+ '- Default aesthetic when the user did not request a theme: light editorial research board, warm off-white paper, precise ink lines, subtle grid/noise, restrained color accents, refined spacing. It should feel closer to a polished team workspace/design note than a dark coding console.',
168
+ '- Aesthetic: choose a committed direction such as editorial lab notebook, precision engineering console, warm classroom blackboard, industrial systems map, or high-contrast research poster. Avoid generic SaaS cards.',
169
+ '- Typography: use distinctive CSS font stacks from local system fonts only, for example Georgia/Times editorial serif plus ui-monospace accents, or Avenir/Helvetica Neue with a mono data layer. Do not use plain Inter/Roboto/Arial defaults.',
170
+ '- Color: define CSS variables in :root. Use a restrained but intentional palette with textured background, grid/noise/paper effect, or layered panels. Avoid flat dark gray, neon cyan buttons, purple gradients, and default dark-mode dashboards unless the content explicitly requires them.',
171
+ '- Polish: use fine borders, soft shadows, intentional whitespace, aligned baselines, and varied panel sizes. Do not make every block the same rounded rectangle.',
172
+ '- SVG: include at least one large inline SVG with labeled nodes, arrows, and a visible animated path. Arrow animation must be obvious: stroke-dasharray/stroke-dashoffset or moving marker/dot.',
173
+ '- Motion: include 2-4 purposeful animations: entry reveal, arrow/path draw, active step highlight, formula/example transition. Respect prefers-reduced-motion.',
174
+ '- Explanation: every diagram node must have adjacent text that explains why it matters. Do not rely on labels alone.',
175
+ '- Example: include a minimal concrete example with inputs, intermediate transformation, and output/result.',
176
+ '- Density: avoid empty hero-only pages. The first viewport should already contain the main diagram and explanatory panels.',
177
+ '- Controls: provide Replay / Step controls when animation or process is present.',
178
+ '- Finish: no placeholder lorem ipsum, no “template” language, no meta comments about being generated.',
179
+ zh
180
+ ? '- Chinese copy must be concise, concrete, and tied to the user/agent answer. Do not use “苏格拉底/架构概念/问题分解/完整答案” as HTML section names unless the user explicitly asks.'
181
+ : '- Copy must be concise, concrete, and tied to the user/agent answer. Do not use generic section names unless the user explicitly asks.',
182
+ ].join('\n');
183
+ }
184
+
96
185
  function parseMetadata(metadata) {
97
186
  if (!metadata) return null;
98
187
  if (typeof metadata === 'object') return metadata;
package/src/runner.js CHANGED
@@ -14,7 +14,7 @@ import { log } from './logger.js';
14
14
  import { buildRuntimeInfo } from './runtime-info.js';
15
15
  import { runShellCommand } from './shell-runner.js';
16
16
  import { TerminalSessionManager } from './terminal-session.js';
17
- import { buildOpenDesignPrompt, chooseOpenDesignSkills, prepareOpenDesignDir, shouldRenderOpenDesignArtifact } from './opendesign.js';
17
+ import { buildOpenDesignPrompt, buildOpenDesignRepairPrompt, chooseOpenDesignSkills, evaluateOpenDesignArtifact, prepareOpenDesignDir, shouldRenderOpenDesignArtifact } from './opendesign.js';
18
18
 
19
19
  const TERMINAL_STATUSES = new Set(['review', 'done', 'approved', 'cancelled']);
20
20
 
@@ -288,13 +288,14 @@ export class Runner {
288
288
  });
289
289
  const outputDir = await prepareOpenDesignDir(this.config, topicId);
290
290
  const skills = chooseOpenDesignSkills(message, reply);
291
+ const locale = inferLocale(`${message.content || ''}\n${reply || ''}`);
291
292
  const prompt = await buildOpenDesignPrompt({
292
293
  config: this.config,
293
294
  outputDir,
294
295
  userMessage: message.content || '',
295
296
  reply,
296
297
  topicId,
297
- locale: inferLocale(`${message.content || ''}\n${reply || ''}`),
298
+ locale,
298
299
  skills,
299
300
  });
300
301
  await adapter.run(prompt, {
@@ -302,6 +303,23 @@ export class Runner {
302
303
  topicId,
303
304
  onProgress: (event) => this.maybePublishProgress(topicId, event, adapter.name),
304
305
  });
306
+ let quality = await evaluateOpenDesignArtifact(outputDir);
307
+ if (!quality.ok) {
308
+ log('warn', 'opendesign artifact quality check failed; requesting rewrite', { topicId, score: quality.score, issues: quality.issues.join('; ') });
309
+ await this.wtt.typing(topicId, 'start', {
310
+ statusText: 'OpenDesign 正在重写低质量 artifact',
311
+ statusKind: 'opendesign_repair',
312
+ adapter: adapter.name,
313
+ ttlMs: 45000,
314
+ });
315
+ await adapter.run(buildOpenDesignRepairPrompt({ outputDir, issues: quality.issues, locale }), {
316
+ sessionKey: `wtt:opendesign:${topicId}`,
317
+ topicId,
318
+ onProgress: (event) => this.maybePublishProgress(topicId, event, adapter.name),
319
+ });
320
+ quality = await evaluateOpenDesignArtifact(outputDir);
321
+ log(quality.ok ? 'info' : 'warn', 'opendesign artifact quality check after rewrite', { topicId, score: quality.score, issues: quality.issues.join('; ') });
322
+ }
305
323
  const artifact = await this.artifacts.uploadDirectory(outputDir, {
306
324
  source: messageTopicType(message) || 'feed',
307
325
  sourceId: topicId,
@@ -142,6 +142,30 @@ export function logs(argv) {
142
142
  throw new Error(`unsupported platform: ${process.platform}`);
143
143
  }
144
144
 
145
+ export function workdir(argv) {
146
+ const [profile = '', nextDir = ''] = argv._ || [];
147
+ if (!profile) throw new Error('usage: wtt-connect workdir <profile> [path] [--restart]');
148
+ const safe = sanitizeProfile(profile);
149
+ const envFile = profileEnvFile(safe);
150
+ if (!fs.existsSync(envFile)) throw new Error(`profile not found: ${safe}`);
151
+ const env = readEnv(envFile);
152
+ if (!nextDir) {
153
+ console.log(env.WTT_CONNECT_WORKDIR || '');
154
+ return;
155
+ }
156
+ const resolved = path.resolve(nextDir);
157
+ ensureDir(resolved);
158
+ env.WTT_CONNECT_WORKDIR = resolved;
159
+ writeProfileEnv(envFile, env);
160
+ console.log(`updated ${safe} workdir: ${resolved}`);
161
+ if (argv.restart) {
162
+ restartService(safe);
163
+ console.log(`${safe}: ${serviceStatus(safe)}`);
164
+ } else {
165
+ console.log(`restart required: wtt-connect restart ${safe}`);
166
+ }
167
+ }
168
+
145
169
  export function down(argv) {
146
170
  const [profile = ''] = argv._ || [];
147
171
  if (!profile) throw new Error('usage: wtt-connect down <profile>');