wtt-connect 0.2.63 → 0.2.65

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.63",
3
+ "version": "0.2.65",
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,6 +10,7 @@ import { smokeChat, smokeTask } from './smoke.js';
10
10
  import { ArtifactManager } from './artifacts.js';
11
11
  import { DurableStore } from './store.js';
12
12
  import { WTTClient } from './wtt-client.js';
13
+ import { WTTApi } from './wtt-api.js';
13
14
  import { log, redact } from './logger.js';
14
15
  import { adapterBin, normalizeProfileName } from './adapters/generic-cli.js';
15
16
  import { normalizeAdapterName } from './adapters/index.js';
@@ -38,6 +39,7 @@ export async function main(args) {
38
39
  if (cmd === 'upload-artifact' || cmd === 'opendesign-upload') return uploadArtifact(loadConfig(argv), argv);
39
40
  if (cmd === 'preview-port' || cmd === 'sandbox-preview') return previewPort(loadConfig(argv), argv);
40
41
  if (cmd === 'cleanup-previews' || cmd === 'preview-cleanup') return cleanupPreviewsCommand(loadConfig(argv), argv);
42
+ if (cmd === 'feishu' || cmd === 'lark') return feishuCommand(loadConfig(argv), argv);
41
43
  if (cmd === 'start') {
42
44
  const config = loadConfig(argv);
43
45
  if (!config.agentId) throw new Error('WTT_AGENT_ID is required');
@@ -92,6 +94,9 @@ function parseArgs(args) {
92
94
  else if (a === '--source') out.source = args[++i];
93
95
  else if (a === '--source-id') out.sourceId = args[++i];
94
96
  else if (a === '--title') out.title = args[++i];
97
+ else if (a === '--content') out.content = args[++i];
98
+ else if (a === '--folder-token') out.folderToken = args[++i];
99
+ else if (a === '--max-chars') out.maxChars = Number(args[++i]);
95
100
  else if (a === '--entry') out.entry = args[++i];
96
101
  else if (a === '--type') out.type = args[++i];
97
102
  else if (a === '--port') out.port = Number(args[++i]);
@@ -151,10 +156,62 @@ Commands:
151
156
  Create a Cloud Sandbox live preview URL; snapshots are opt-in
152
157
  cleanup-previews [--keep-last <n>]
153
158
  Stop preview servers previously registered by this agent
159
+ feishu read-doc <url-or-token> [--max-chars n]
160
+ Read a Feishu/Lark doc shared with the WTT Bot
161
+ feishu create-doc --title <title> [--content text|--file path] [--folder-token token]
162
+ Create a Feishu/Lark doc through the WTT Bot
163
+ feishu append-doc <url-or-token> (--content text|--file path)
164
+ Append text to a Feishu/Lark doc through the WTT Bot
154
165
  help Show this help
155
166
  `);
156
167
  }
157
168
 
169
+ async function feishuCommand(config, argv) {
170
+ const subcommand = String(argv._[0] || '').trim().toLowerCase();
171
+ const api = new WTTApi(config);
172
+ if (!config.agentId) throw new Error('WTT_AGENT_ID is required');
173
+ if (!config.token) throw new Error('WTT_TOKEN or WTT_AGENT_TOKEN is required');
174
+ if (subcommand === 'read-doc' || subcommand === 'read') {
175
+ const urlOrToken = String(argv._[1] || argv.url || argv.token || '').trim();
176
+ if (!urlOrToken) throw new Error('feishu read-doc requires <url-or-token>');
177
+ const result = await api.feishuReadDoc({ urlOrToken, maxChars: argv.maxChars });
178
+ console.log(JSON.stringify(result, null, 2));
179
+ return;
180
+ }
181
+ if (subcommand === 'create-doc' || subcommand === 'create') {
182
+ const title = String(argv.title || '').trim();
183
+ if (!title) throw new Error('feishu create-doc requires --title <title>');
184
+ const content = readCommandContent(argv);
185
+ const result = await api.feishuCreateDoc({ title, content, folderToken: argv.folderToken || '' });
186
+ console.log(JSON.stringify(result, null, 2));
187
+ return;
188
+ }
189
+ if (subcommand === 'append-doc' || subcommand === 'append') {
190
+ const urlOrToken = String(argv._[1] || argv.url || argv.token || '').trim();
191
+ if (!urlOrToken) throw new Error('feishu append-doc requires <url-or-token>');
192
+ const content = readCommandContent(argv);
193
+ if (!content.trim()) throw new Error('feishu append-doc requires --content <text> or --file <path>');
194
+ const result = await api.feishuAppendDoc({ urlOrToken, content });
195
+ console.log(JSON.stringify(result, null, 2));
196
+ return;
197
+ }
198
+ throw new Error([
199
+ 'unknown feishu command',
200
+ 'Usage:',
201
+ ' wtt-connect feishu read-doc <url-or-token> [--max-chars n]',
202
+ ' wtt-connect feishu create-doc --title <title> [--content text|--file path] [--folder-token token]',
203
+ ' wtt-connect feishu append-doc <url-or-token> (--content text|--file path)',
204
+ ].join('\n'));
205
+ }
206
+
207
+ function readCommandContent(argv) {
208
+ if (typeof argv.content === 'string') return argv.content;
209
+ const file = Array.isArray(argv.files) ? argv.files[0] : '';
210
+ if (!file) return '';
211
+ const resolved = path.resolve(String(file));
212
+ return fs.readFileSync(resolved, 'utf8');
213
+ }
214
+
158
215
  async function previewPort(config, argv) {
159
216
  const port = Number(argv.port || argv._[0]);
160
217
  if (!Number.isInteger(port) || port < 1024 || port > 65535 || port === 3000) {
package/src/runner.js CHANGED
@@ -228,8 +228,9 @@ export class Runner {
228
228
  return;
229
229
  }
230
230
  }
231
+ const skillSlashPrompt = isSlashPassthrough ? this.skillSlashPrompt(content, m.metadata) : '';
231
232
  const compactSummary = this.store.getSession(sessionKey).compactSummary || '';
232
- const prompt = isSlashPassthrough ? content : [
233
+ const prompt = isSlashPassthrough ? (skillSlashPrompt || content) : [
233
234
  'You are replying to a WTT Web conversation. Do not mention implementation internals unless asked.',
234
235
  `WTT topic_id: ${topicId}`,
235
236
  `WTT topic_type: ${messageTopicType(m) || 'unknown'}`,
@@ -253,6 +254,7 @@ export class Runner {
253
254
  staged.promptBlock,
254
255
  renderTranscriptBlock(transcripts),
255
256
  renderCloudSandboxStorageInstruction(this.config, topicId),
257
+ renderFeishuToolInstruction(this.config),
256
258
  renderGeneratedFileArtifactInstruction(this.config, topicId),
257
259
  'Reply naturally and concisely unless the user asks for detail.',
258
260
  ].filter(Boolean).join('\n');
@@ -434,6 +436,12 @@ export class Runner {
434
436
  return `${adapterDisplayName(adapter.name)} session cleared for this WTT topic. The next turn starts a fresh agent thread/session.`;
435
437
  }
436
438
 
439
+ skillSlashPrompt(content, metadata = null) {
440
+ const runtime = this.runtimeInfo();
441
+ const commands = Array.isArray(runtime.skill_commands) ? runtime.skill_commands : [];
442
+ return buildSkillSlashPrompt(content, metadata, commands);
443
+ }
444
+
437
445
  async compactAdapterSession(adapter, context = {}, command = '/compact', args = '') {
438
446
  const sessionKey = context.sessionKey || 'default';
439
447
  const topicId = String(context.topicId || '').trim();
@@ -764,6 +772,33 @@ export class Runner {
764
772
  }
765
773
  }
766
774
 
775
+ export function buildSkillSlashPrompt(content, metadata = null, commands = []) {
776
+ const parsed = parseSlashCommand(content);
777
+ if (!parsed) return '';
778
+ const meta = parseMetadata(metadata);
779
+ const metadataSkillId = String(meta?.skill_id || meta?.skillId || '').trim();
780
+ const metadataFamily = String(meta?.command_family || meta?.commandFamily || '').trim().toLowerCase();
781
+ const matched = (Array.isArray(commands) ? commands : []).find((item) => String(item?.cmd || '').trim().toLowerCase() === parsed.command)
782
+ || (metadataFamily === 'skill' || metadataSkillId ? {
783
+ cmd: parsed.command,
784
+ skill_id: metadataSkillId || parsed.command.replace(/^\/+/, ''),
785
+ name: metadataSkillId || parsed.command.replace(/^\/+/, ''),
786
+ desc: '',
787
+ } : null);
788
+ if (!matched) return '';
789
+ const skillName = String(matched.name || matched.skill_id || matched.cmd || '').trim().replace(/^\/+/, '');
790
+ const skillId = String(matched.skill_id || skillName).trim();
791
+ const args = String(parsed.args || '').trim();
792
+ return [
793
+ `Use the ${skillName} skill${skillId && skillId !== skillName ? ` (${skillId})` : ''}.`,
794
+ matched.desc ? `Skill description: ${matched.desc}` : null,
795
+ '',
796
+ args ? `User request: ${args}` : 'User request: Run this skill for the current conversation.',
797
+ '',
798
+ `Original slash command: ${content}`,
799
+ ].filter(Boolean).join('\n');
800
+ }
801
+
767
802
  function adapterDisplayName(name) {
768
803
  if (name === 'claude-code') return 'Claude Code';
769
804
  if (name === 'codex') return 'Codex';
@@ -1108,6 +1143,21 @@ function renderGeneratedFileArtifactInstruction(config, topicId = '') {
1108
1143
  ].join('\n');
1109
1144
  }
1110
1145
 
1146
+ function renderFeishuToolInstruction(config) {
1147
+ if (!config.agentId || !config.token) return '';
1148
+ return [
1149
+ 'Feishu/Lark document tool rule, for internal execution only:',
1150
+ '- If the user asks you to read, summarize, analyze, create, or append a Feishu/Lark document, or provides a docs.feishu.cn / feishu.cn / larksuite.com docx/wiki link, call the local WTT Feishu tool instead of guessing from the URL.',
1151
+ '- Read a document: `wtt-connect feishu read-doc "<feishu_doc_or_wiki_url>" --max-chars 50000`.',
1152
+ '- Create a document: `wtt-connect feishu create-doc --title "Title" --content "text"` or put larger content in a file and use `--file path`.',
1153
+ '- Append to a document: `wtt-connect feishu append-doc "<feishu_doc_or_wiki_url>" --content "text"` or `--file path`.',
1154
+ '- The backend only proxies explicit tool calls; do not claim you accessed a Feishu document unless the command succeeds.',
1155
+ '- If the command returns a permission error, tell the user to share the document with the WTT Bot or grant the WTT Feishu app cloud document permissions.',
1156
+ '- The first version intentionally supports read/create/append only. Do not attempt destructive delete, clear, or overwrite operations.',
1157
+ '- When the user explicitly asks to write/create/append a Feishu document, proceed without asking for an extra confirmation.',
1158
+ ].join('\n');
1159
+ }
1160
+
1111
1161
  function renderCloudSandboxStorageInstruction(config, topicId = '') {
1112
1162
  if (!config.cloudSandbox) return '';
1113
1163
  const workDir = config.workDir || process.cwd();
@@ -1611,6 +1661,7 @@ function buildTaskPrompt(task, config, staged = { promptBlock: '' }, transcripts
1611
1661
  '',
1612
1662
  config.requireCommitPush ? 'For code changes, commit and push before final response, and include commit id.' : '',
1613
1663
  renderCloudSandboxStorageInstruction(config, task.topic_id || task.topicId || ''),
1664
+ renderFeishuToolInstruction(config),
1614
1665
  renderGeneratedFileArtifactInstruction(config, task.topic_id || task.topicId || ''),
1615
1666
  'Return a concise final summary with evidence, changed files, tests, artifacts, and blockers.',
1616
1667
  ].filter(Boolean).join('\n');
@@ -3,6 +3,9 @@ import path from 'node:path';
3
3
  import fs from 'node:fs';
4
4
  import { execFileSync } from 'node:child_process';
5
5
 
6
+ let skillCommandCache = { at: 0, commands: [] };
7
+ const SKILL_COMMAND_CACHE_MS = 60_000;
8
+
6
9
  export function buildRuntimeInfo(config, runtimeState = {}) {
7
10
  const workdir = resolveWorkDir(config.workDir);
8
11
  const git = gitInfo(workdir);
@@ -30,10 +33,145 @@ export function buildRuntimeInfo(config, runtimeState = {}) {
30
33
  node: process.version,
31
34
  mode: config.mode,
32
35
  git,
36
+ skill_commands: discoverSkillCommands(config, workdir),
33
37
  updated_at: new Date().toISOString(),
34
38
  };
35
39
  }
36
40
 
41
+ export function discoverSkillCommands(config = {}, workdir = resolveWorkDir(config.workDir)) {
42
+ const now = Date.now();
43
+ if (now - skillCommandCache.at < SKILL_COMMAND_CACHE_MS) return skillCommandCache.commands;
44
+ const roots = skillRoots(config, workdir);
45
+ const seenCommands = new Set();
46
+ const commands = [];
47
+ for (const root of roots) {
48
+ for (const skillFile of findSkillFiles(root, 5)) {
49
+ const parsed = parseSkillFile(skillFile);
50
+ if (!parsed.id) continue;
51
+ const commandName = normalizeSkillCommandName(parsed.name || parsed.id);
52
+ if (!commandName) continue;
53
+ const cmd = `/${commandName}`;
54
+ const key = cmd.toLowerCase();
55
+ if (seenCommands.has(key)) continue;
56
+ seenCommands.add(key);
57
+ commands.push({
58
+ cmd,
59
+ desc: parsed.description || `Use ${parsed.name || parsed.id} skill`,
60
+ family: 'skill',
61
+ mode: 'passthrough',
62
+ skill_id: parsed.id,
63
+ name: parsed.name || parsed.id,
64
+ source: parsed.source,
65
+ });
66
+ if (commands.length >= 120) break;
67
+ }
68
+ if (commands.length >= 120) break;
69
+ }
70
+ skillCommandCache = { at: now, commands };
71
+ return commands;
72
+ }
73
+
74
+ function skillRoots(config = {}, workdir = '') {
75
+ const home = os.homedir();
76
+ const roots = [
77
+ path.join(home, '.codex', 'skills'),
78
+ path.join(home, '.agents', 'skills'),
79
+ path.join(home, '.claude', 'skills'),
80
+ path.join(home, '.claude', 'mattpocock-skills', 'skills'),
81
+ workdir ? path.join(workdir, '.codex', 'skills') : '',
82
+ workdir ? path.join(workdir, '.claude', 'skills') : '',
83
+ process.cwd() ? path.join(process.cwd(), '.codex', 'skills') : '',
84
+ process.cwd() ? path.join(process.cwd(), '.claude', 'skills') : '',
85
+ ];
86
+ const extra = String(process.env.WTT_CONNECT_SKILL_ROOTS || config.skillRoots || '').split(path.delimiter);
87
+ return Array.from(new Set(
88
+ [...roots, ...extra]
89
+ .map((item) => String(item || '').trim())
90
+ .filter(Boolean)
91
+ .map((item) => path.resolve(item))
92
+ ));
93
+ }
94
+
95
+ function findSkillFiles(root, maxDepth = 4) {
96
+ const out = [];
97
+ if (!root || !fs.existsSync(root)) return out;
98
+ const visit = (dir, depth) => {
99
+ if (depth < 0 || out.length >= 160) return;
100
+ let entries = [];
101
+ try {
102
+ entries = fs.readdirSync(dir, { withFileTypes: true });
103
+ } catch {
104
+ return;
105
+ }
106
+ if (entries.some((entry) => entry.isFile() && entry.name === 'SKILL.md')) {
107
+ out.push(path.join(dir, 'SKILL.md'));
108
+ return;
109
+ }
110
+ for (const entry of entries) {
111
+ if (!entry.isDirectory()) continue;
112
+ if (entry.name === 'node_modules' || entry.name.startsWith('.git')) continue;
113
+ visit(path.join(dir, entry.name), depth - 1);
114
+ if (out.length >= 160) return;
115
+ }
116
+ };
117
+ visit(root, maxDepth);
118
+ return out;
119
+ }
120
+
121
+ function parseSkillFile(file) {
122
+ try {
123
+ const text = fs.readFileSync(file, 'utf8');
124
+ const fallbackId = path.basename(path.dirname(file));
125
+ const frontmatter = text.match(/^---\s*\n([\s\S]*?)\n---/);
126
+ const meta = frontmatter ? parseSimpleYaml(frontmatter[1]) : {};
127
+ const id = String(meta.name || fallbackId || '').trim();
128
+ return {
129
+ id,
130
+ name: id,
131
+ description: String(meta.description || firstMarkdownSentence(text) || '').trim(),
132
+ source: sourceForSkillPath(file),
133
+ };
134
+ } catch {
135
+ return { id: '', name: '', description: '', source: 'local' };
136
+ }
137
+ }
138
+
139
+ function parseSimpleYaml(text) {
140
+ const out = {};
141
+ for (const line of String(text || '').split(/\r?\n/)) {
142
+ const match = line.match(/^([A-Za-z0-9_-]+)\s*:\s*(.*)$/);
143
+ if (!match) continue;
144
+ const key = match[1];
145
+ let value = match[2].trim();
146
+ value = value.replace(/^['"]|['"]$/g, '');
147
+ out[key] = value;
148
+ }
149
+ return out;
150
+ }
151
+
152
+ function firstMarkdownSentence(text) {
153
+ const body = String(text || '').replace(/^---\s*\n[\s\S]*?\n---/, '').trim();
154
+ const line = body.split(/\r?\n/).map((item) => item.trim().replace(/^#+\s*/, '')).find(Boolean);
155
+ return line || '';
156
+ }
157
+
158
+ function normalizeSkillCommandName(value) {
159
+ const raw = String(value || '').trim().split(':').pop() || '';
160
+ return raw
161
+ .replace(/[^A-Za-z0-9_-]+/g, '-')
162
+ .replace(/^-+|-+$/g, '')
163
+ .slice(0, 80)
164
+ .toLowerCase();
165
+ }
166
+
167
+ function sourceForSkillPath(file) {
168
+ const normalized = String(file || '');
169
+ if (normalized.includes(`${path.sep}.codex${path.sep}`)) return 'codex-local';
170
+ if (normalized.includes(`${path.sep}.claude${path.sep}`)) return 'claude-local';
171
+ if (normalized.includes(`${path.sep}.agents${path.sep}`)) return 'agents-local';
172
+ return 'local';
173
+ }
174
+
37
175
  function runtimeModel(config, adapter, runtimeState = {}) {
38
176
  const fromRun = String(runtimeState.current_model || runtimeState.model || '').trim();
39
177
  if (fromRun) return fromRun;
package/src/wtt-api.js CHANGED
@@ -130,4 +130,38 @@ export class WTTApi {
130
130
  }
131
131
  }
132
132
 
133
+ async feishuReadDoc({ urlOrToken, maxChars } = {}) {
134
+ return this.request('POST', '/integrations/feishu/agent/doc/read', {
135
+ headers: this.agentHeaders(),
136
+ json: {
137
+ agent_id: this.config.agentId,
138
+ url_or_token: urlOrToken,
139
+ ...(maxChars ? { max_chars: Number(maxChars) } : {}),
140
+ },
141
+ });
142
+ }
143
+
144
+ async feishuCreateDoc({ title, content = '', folderToken = '' } = {}) {
145
+ return this.request('POST', '/integrations/feishu/agent/doc/create', {
146
+ headers: this.agentHeaders(),
147
+ json: {
148
+ agent_id: this.config.agentId,
149
+ title,
150
+ content,
151
+ ...(folderToken ? { folder_token: folderToken } : {}),
152
+ },
153
+ });
154
+ }
155
+
156
+ async feishuAppendDoc({ urlOrToken, content } = {}) {
157
+ return this.request('POST', '/integrations/feishu/agent/doc/append', {
158
+ headers: this.agentHeaders(),
159
+ json: {
160
+ agent_id: this.config.agentId,
161
+ url_or_token: urlOrToken,
162
+ content,
163
+ },
164
+ });
165
+ }
166
+
133
167
  }