wtt-connect 0.2.63 → 0.2.64

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.64",
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/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'}`,
@@ -434,6 +435,12 @@ export class Runner {
434
435
  return `${adapterDisplayName(adapter.name)} session cleared for this WTT topic. The next turn starts a fresh agent thread/session.`;
435
436
  }
436
437
 
438
+ skillSlashPrompt(content, metadata = null) {
439
+ const runtime = this.runtimeInfo();
440
+ const commands = Array.isArray(runtime.skill_commands) ? runtime.skill_commands : [];
441
+ return buildSkillSlashPrompt(content, metadata, commands);
442
+ }
443
+
437
444
  async compactAdapterSession(adapter, context = {}, command = '/compact', args = '') {
438
445
  const sessionKey = context.sessionKey || 'default';
439
446
  const topicId = String(context.topicId || '').trim();
@@ -764,6 +771,33 @@ export class Runner {
764
771
  }
765
772
  }
766
773
 
774
+ export function buildSkillSlashPrompt(content, metadata = null, commands = []) {
775
+ const parsed = parseSlashCommand(content);
776
+ if (!parsed) return '';
777
+ const meta = parseMetadata(metadata);
778
+ const metadataSkillId = String(meta?.skill_id || meta?.skillId || '').trim();
779
+ const metadataFamily = String(meta?.command_family || meta?.commandFamily || '').trim().toLowerCase();
780
+ const matched = (Array.isArray(commands) ? commands : []).find((item) => String(item?.cmd || '').trim().toLowerCase() === parsed.command)
781
+ || (metadataFamily === 'skill' || metadataSkillId ? {
782
+ cmd: parsed.command,
783
+ skill_id: metadataSkillId || parsed.command.replace(/^\/+/, ''),
784
+ name: metadataSkillId || parsed.command.replace(/^\/+/, ''),
785
+ desc: '',
786
+ } : null);
787
+ if (!matched) return '';
788
+ const skillName = String(matched.name || matched.skill_id || matched.cmd || '').trim().replace(/^\/+/, '');
789
+ const skillId = String(matched.skill_id || skillName).trim();
790
+ const args = String(parsed.args || '').trim();
791
+ return [
792
+ `Use the ${skillName} skill${skillId && skillId !== skillName ? ` (${skillId})` : ''}.`,
793
+ matched.desc ? `Skill description: ${matched.desc}` : null,
794
+ '',
795
+ args ? `User request: ${args}` : 'User request: Run this skill for the current conversation.',
796
+ '',
797
+ `Original slash command: ${content}`,
798
+ ].filter(Boolean).join('\n');
799
+ }
800
+
767
801
  function adapterDisplayName(name) {
768
802
  if (name === 'claude-code') return 'Claude Code';
769
803
  if (name === 'codex') return 'Codex';
@@ -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;