viepilot 2.50.1 → 3.7.2
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/CHANGELOG.md +204 -0
- package/README.md +1 -1
- package/bin/viepilot.cjs +1 -0
- package/bin/vp-tools.cjs +123 -1
- package/docs/brainstorm/session-2026-05-22.md +472 -0
- package/docs/dev/agents.md +51 -41
- package/lib/adapter-context.cjs +294 -0
- package/lib/adapters/antigravity.cjs +8 -2
- package/lib/adapters/claude-code.cjs +4 -0
- package/lib/audit/browser-runner.cjs +102 -0
- package/lib/intake/adapters/browser.cjs +58 -0
- package/lib/intake/adapters/excel-m365.cjs +54 -6
- package/lib/intake/auto-intake.cjs +194 -0
- package/lib/intake/classifier.cjs +22 -4
- package/lib/intake/manifest.cjs +81 -0
- package/lib/intake/triage-ux.cjs +10 -2
- package/lib/intake/validator.cjs +97 -0
- package/lib/intake/writeback.cjs +169 -3
- package/lib/request/url-enricher.cjs +69 -0
- package/lib/viepilot-install.cjs +15 -0
- package/package.json +1 -1
- package/skills/vp-audit/SKILL.md +99 -3
- package/skills/vp-auto/SKILL.md +54 -4
- package/skills/vp-brainstorm/SKILL.md +69 -3
- package/skills/vp-crystallize/SKILL.md +52 -3
- package/skills/vp-debug/SKILL.md +52 -3
- package/skills/vp-design/SKILL.md +52 -3
- package/skills/vp-docs/SKILL.md +52 -3
- package/skills/vp-evolve/SKILL.md +52 -3
- package/skills/vp-info/SKILL.md +52 -3
- package/skills/vp-intake/SKILL.md +306 -7
- package/skills/vp-pause/SKILL.md +52 -3
- package/skills/vp-persona/SKILL.md +52 -3
- package/skills/vp-proposal/SKILL.md +52 -3
- package/skills/vp-request/SKILL.md +72 -3
- package/skills/vp-resume/SKILL.md +52 -3
- package/skills/vp-rollback/SKILL.md +52 -3
- package/skills/vp-skills/SKILL.md +52 -3
- package/skills/vp-status/SKILL.md +52 -3
- package/skills/vp-task/SKILL.md +52 -3
- package/skills/vp-ui-components/SKILL.md +52 -3
- package/skills/vp-update/SKILL.md +52 -3
- package/workflows/autonomous.md +268 -18
- package/workflows/brainstorm.md +222 -7
- package/workflows/crystallize.md +124 -6
- package/workflows/design.md +62 -1
- package/workflows/request.md +54 -8
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ADAPTER_CONTEXT — per-adapter capability map for ViePilot v3.
|
|
5
|
+
*
|
|
6
|
+
* Each adapter entry defines:
|
|
7
|
+
* tools{} — canonical tool name for each abstract operation
|
|
8
|
+
* interactive — "AUQ" | "text" | "text-plan-only" | "none"
|
|
9
|
+
* orchestration{} — parallel dispatch capability
|
|
10
|
+
* hooks{} — hook event support
|
|
11
|
+
* mcp{} — MCP support constraints
|
|
12
|
+
* subagent — "multi-level" | "single-level" | "command-only" | "none"
|
|
13
|
+
*
|
|
14
|
+
* Skills read ADAPTER_CONTEXT instead of maintaining inline compat tables.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const ADAPTER_CONTEXTS = {
|
|
18
|
+
|
|
19
|
+
'claude-code': {
|
|
20
|
+
id: 'claude-code',
|
|
21
|
+
name: 'Claude Code',
|
|
22
|
+
tools: {
|
|
23
|
+
shell: 'Bash',
|
|
24
|
+
read: 'Read',
|
|
25
|
+
write: 'Write',
|
|
26
|
+
edit: 'Edit',
|
|
27
|
+
multi_edit: 'MultiEdit',
|
|
28
|
+
search: 'Grep',
|
|
29
|
+
glob: 'Glob',
|
|
30
|
+
ls: 'LS',
|
|
31
|
+
web_search: 'WebSearch',
|
|
32
|
+
web_fetch: 'WebFetch',
|
|
33
|
+
notebook_read: 'NotebookRead',
|
|
34
|
+
notebook_edit: 'NotebookEdit',
|
|
35
|
+
tool_search: 'ToolSearch',
|
|
36
|
+
todo_read: 'TodoRead',
|
|
37
|
+
todo_write: 'TodoWrite',
|
|
38
|
+
agent: 'Agent',
|
|
39
|
+
interactive: 'AskUserQuestion', // deferred — preload via ToolSearch first
|
|
40
|
+
},
|
|
41
|
+
interactive: 'AUQ', // must call ToolSearch before first AUQ
|
|
42
|
+
subagent: 'multi-level', // Agent tool supports nested spawning
|
|
43
|
+
orchestration: {
|
|
44
|
+
mode: 'agent-tool', // Agent tool = callable from skill
|
|
45
|
+
parallel: true,
|
|
46
|
+
teams: true, // CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
|
|
47
|
+
background: true,
|
|
48
|
+
model_override: {
|
|
49
|
+
worker: 'claude-haiku-4-5',
|
|
50
|
+
orchestrator: 'claude-sonnet-4-6',
|
|
51
|
+
},
|
|
52
|
+
max_parallel_tasks: 5,
|
|
53
|
+
},
|
|
54
|
+
hooks: {
|
|
55
|
+
count: 28,
|
|
56
|
+
supported_events: [
|
|
57
|
+
'SessionStart', 'SessionEnd', 'Stop', 'StopFailure',
|
|
58
|
+
'UserPromptSubmit', 'PreToolUse', 'PostToolUse', 'PostToolUseFailure',
|
|
59
|
+
'FileChanged', 'SubagentStart', 'SubagentStop',
|
|
60
|
+
'TaskCreated', 'TaskCompleted', 'PreCompact', 'PostCompact',
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
mcp: { supported: true, tool_limit: null },
|
|
64
|
+
skill_path_project: '.claude/skills',
|
|
65
|
+
skill_path_global: '~/.claude/skills',
|
|
66
|
+
agents_dir: '.claude/agents',
|
|
67
|
+
},
|
|
68
|
+
|
|
69
|
+
'cursor-agent': {
|
|
70
|
+
id: 'cursor-agent',
|
|
71
|
+
name: 'Cursor (Agent Mode)',
|
|
72
|
+
tools: {
|
|
73
|
+
shell: 'run_terminal_cmd',
|
|
74
|
+
read: 'read_file',
|
|
75
|
+
write: 'edit_file',
|
|
76
|
+
edit: 'edit_file',
|
|
77
|
+
multi_edit: 'edit_file',
|
|
78
|
+
search: 'grep_search',
|
|
79
|
+
glob: 'file_search',
|
|
80
|
+
ls: 'list_dir',
|
|
81
|
+
web_search: 'web_search',
|
|
82
|
+
web_fetch: null, // not available
|
|
83
|
+
notebook_read: null,
|
|
84
|
+
notebook_edit: null,
|
|
85
|
+
tool_search: null,
|
|
86
|
+
todo_read: null,
|
|
87
|
+
todo_write: null,
|
|
88
|
+
agent: null, // /multitask is user command, not callable tool
|
|
89
|
+
interactive: null, // AskQuestion only in Plan Mode
|
|
90
|
+
},
|
|
91
|
+
interactive: 'text', // plain-text numbered list fallback
|
|
92
|
+
subagent: 'command-only', // /multitask user cmd, single-level, not callable
|
|
93
|
+
orchestration: {
|
|
94
|
+
mode: 'sequential',
|
|
95
|
+
parallel: false,
|
|
96
|
+
teams: false,
|
|
97
|
+
background: false,
|
|
98
|
+
model_override: null,
|
|
99
|
+
max_parallel_tasks: 1,
|
|
100
|
+
},
|
|
101
|
+
hooks: {
|
|
102
|
+
count: 5,
|
|
103
|
+
supported_events: [
|
|
104
|
+
'beforeShellExecution', 'beforeMCPExecution',
|
|
105
|
+
'beforeReadFile', 'afterFileEdit', 'stop',
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
mcp: { supported: true, tool_limit: 40 },
|
|
109
|
+
skill_path_project: '.cursor/skills',
|
|
110
|
+
skill_path_global: '~/.cursor/skills',
|
|
111
|
+
agents_dir: null,
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
'antigravity': {
|
|
115
|
+
id: 'antigravity',
|
|
116
|
+
name: 'Antigravity (Google)',
|
|
117
|
+
tools: {
|
|
118
|
+
shell: 'shell',
|
|
119
|
+
read: 'file_read',
|
|
120
|
+
write: 'file_write',
|
|
121
|
+
edit: 'file_write',
|
|
122
|
+
multi_edit: 'file_write',
|
|
123
|
+
search: null, // via shell or MCP
|
|
124
|
+
glob: null, // via shell
|
|
125
|
+
ls: null, // via shell
|
|
126
|
+
web_search: null, // via MCP plugin
|
|
127
|
+
web_fetch: null, // via MCP plugin
|
|
128
|
+
notebook_read: null,
|
|
129
|
+
notebook_edit: null,
|
|
130
|
+
tool_search: null,
|
|
131
|
+
todo_read: null,
|
|
132
|
+
todo_write: null,
|
|
133
|
+
agent: null, // async TUI dispatch only
|
|
134
|
+
interactive: null, // TUI-based, no formal AUQ
|
|
135
|
+
},
|
|
136
|
+
interactive: 'none',
|
|
137
|
+
subagent: 'none',
|
|
138
|
+
orchestration: {
|
|
139
|
+
mode: 'sequential',
|
|
140
|
+
parallel: false,
|
|
141
|
+
teams: false,
|
|
142
|
+
background: false,
|
|
143
|
+
model_override: null,
|
|
144
|
+
max_parallel_tasks: 1,
|
|
145
|
+
},
|
|
146
|
+
hooks: {
|
|
147
|
+
count: 3,
|
|
148
|
+
supported_events: ['before_tool', 'after_file_edit', 'session_start'],
|
|
149
|
+
},
|
|
150
|
+
mcp: { supported: true, tool_limit: null },
|
|
151
|
+
skill_path_project: '.agents/skills',
|
|
152
|
+
skill_path_global: '~/.gemini/antigravity/skills',
|
|
153
|
+
agents_dir: null,
|
|
154
|
+
deprecation_notice: 'Gemini CLI was deprecated June 18, 2026. Use Antigravity CLI instead.',
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
'codex': {
|
|
158
|
+
id: 'codex',
|
|
159
|
+
name: 'OpenAI Codex CLI',
|
|
160
|
+
tools: {
|
|
161
|
+
shell: 'container.exec', // sandboxed shell
|
|
162
|
+
read: null, // patch-based — no explicit read tool
|
|
163
|
+
write: 'apply_patch',
|
|
164
|
+
edit: 'apply_patch',
|
|
165
|
+
multi_edit: 'apply_patch',
|
|
166
|
+
search: null, // via shell
|
|
167
|
+
glob: null, // via shell
|
|
168
|
+
ls: null, // via shell
|
|
169
|
+
web_search: 'web_search', // native or MCP
|
|
170
|
+
web_fetch: null,
|
|
171
|
+
notebook_read: null,
|
|
172
|
+
notebook_edit: null,
|
|
173
|
+
tool_search: null,
|
|
174
|
+
todo_read: null,
|
|
175
|
+
todo_write: null,
|
|
176
|
+
agent: 'subagent', // subagents supported
|
|
177
|
+
interactive: null, // TUI Tab/Enter injection only
|
|
178
|
+
},
|
|
179
|
+
interactive: 'none',
|
|
180
|
+
subagent: 'single-level',
|
|
181
|
+
orchestration: {
|
|
182
|
+
mode: 'sequential',
|
|
183
|
+
parallel: false,
|
|
184
|
+
teams: false,
|
|
185
|
+
background: false,
|
|
186
|
+
model_override: null,
|
|
187
|
+
max_parallel_tasks: 1,
|
|
188
|
+
},
|
|
189
|
+
hooks: {
|
|
190
|
+
count: 0,
|
|
191
|
+
supported_events: [],
|
|
192
|
+
},
|
|
193
|
+
mcp: { supported: true, tool_limit: null },
|
|
194
|
+
skill_path_project: null,
|
|
195
|
+
skill_path_global: '~/.codex',
|
|
196
|
+
agents_dir: null,
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
'copilot': {
|
|
200
|
+
id: 'copilot',
|
|
201
|
+
name: 'GitHub Copilot',
|
|
202
|
+
tools: {
|
|
203
|
+
shell: 'runCommands',
|
|
204
|
+
read: 'read',
|
|
205
|
+
write: 'editFiles',
|
|
206
|
+
edit: 'edit',
|
|
207
|
+
multi_edit: 'editFiles',
|
|
208
|
+
search: 'code_search',
|
|
209
|
+
glob: null,
|
|
210
|
+
ls: null,
|
|
211
|
+
web_search: null,
|
|
212
|
+
web_fetch: null,
|
|
213
|
+
notebook_read: null,
|
|
214
|
+
notebook_edit: null,
|
|
215
|
+
tool_search: null,
|
|
216
|
+
todo_read: null,
|
|
217
|
+
todo_write: null,
|
|
218
|
+
agent: null, // explore/task are built-in agents, not callable
|
|
219
|
+
// askQuestions available in main agent ONLY — not in subagents (VS Code #293745)
|
|
220
|
+
interactive: 'askQuestions',
|
|
221
|
+
},
|
|
222
|
+
interactive: 'text', // askQuestions not available in subagents
|
|
223
|
+
subagent: 'none',
|
|
224
|
+
orchestration: {
|
|
225
|
+
mode: 'sequential',
|
|
226
|
+
parallel: false,
|
|
227
|
+
teams: false,
|
|
228
|
+
background: false,
|
|
229
|
+
model_override: null,
|
|
230
|
+
max_parallel_tasks: 1,
|
|
231
|
+
},
|
|
232
|
+
hooks: {
|
|
233
|
+
count: 0,
|
|
234
|
+
supported_events: [],
|
|
235
|
+
},
|
|
236
|
+
mcp: { supported: true, tool_limit: null },
|
|
237
|
+
skill_path_project: '.github/agents',
|
|
238
|
+
skill_path_global: null,
|
|
239
|
+
agents_dir: '.github/agents',
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Aliases
|
|
244
|
+
ADAPTER_CONTEXTS['cursor'] = ADAPTER_CONTEXTS['cursor-agent'];
|
|
245
|
+
ADAPTER_CONTEXTS['cursor-ide'] = ADAPTER_CONTEXTS['cursor-agent'];
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Get ADAPTER_CONTEXT for a given adapter ID. Throws if unknown.
|
|
249
|
+
*/
|
|
250
|
+
function getAdapterContext(id) {
|
|
251
|
+
const ctx = ADAPTER_CONTEXTS[id];
|
|
252
|
+
if (!ctx) {
|
|
253
|
+
throw new Error(`Unknown adapter: "${id}". Known: ${Object.keys(ADAPTER_CONTEXTS).filter(k => !['cursor','cursor-ide'].includes(k)).join(', ')}`);
|
|
254
|
+
}
|
|
255
|
+
return ctx;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* List all canonical adapter IDs (no aliases).
|
|
260
|
+
*/
|
|
261
|
+
function listAdapterIds() {
|
|
262
|
+
return ['claude-code', 'cursor-agent', 'antigravity', 'codex', 'copilot'];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Detect the active adapter from environment heuristics.
|
|
267
|
+
* Returns the adapter ID string.
|
|
268
|
+
*/
|
|
269
|
+
function detectAdapter() {
|
|
270
|
+
const env = process.env;
|
|
271
|
+
|
|
272
|
+
// Env-var signals (strongest signals first)
|
|
273
|
+
if (env.CURSOR_TRACE || env.CURSOR_CHANNEL || env.CURSOR_ENABLED) return 'cursor-agent';
|
|
274
|
+
if (env.ANTIGRAVITY_SESSION || env.GEMINI_ANTIGRAVITY_SESSION) return 'antigravity';
|
|
275
|
+
if (env.CODEX_SESSION || env.OPENAI_CODEX_SESSION) return 'codex';
|
|
276
|
+
if (env.GITHUB_COPILOT_AGENT || env.COPILOT_AGENT) return 'copilot';
|
|
277
|
+
|
|
278
|
+
// TERM_PROGRAM / process name signals
|
|
279
|
+
const termProgram = (env.TERM_PROGRAM || '').toLowerCase();
|
|
280
|
+
if (termProgram === 'claude' || termProgram === 'claude-code') return 'claude-code';
|
|
281
|
+
|
|
282
|
+
// ~/.claude directory exists → likely Claude Code
|
|
283
|
+
const os = require('os');
|
|
284
|
+
const fs = require('fs');
|
|
285
|
+
const path = require('path');
|
|
286
|
+
const home = os.homedir();
|
|
287
|
+
if (fs.existsSync(path.join(home, '.claude', 'settings.json'))) return 'claude-code';
|
|
288
|
+
if (fs.existsSync(path.join(home, '.claude'))) return 'claude-code';
|
|
289
|
+
|
|
290
|
+
// Fallback: claude-code (most common in ViePilot context)
|
|
291
|
+
return 'claude-code';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = { ADAPTER_CONTEXTS, getAdapterContext, listAdapterIds, detectAdapter };
|
|
@@ -6,16 +6,21 @@ const fs = require('fs');
|
|
|
6
6
|
module.exports = {
|
|
7
7
|
id: 'antigravity',
|
|
8
8
|
name: 'Antigravity',
|
|
9
|
+
// Global install path (unchanged)
|
|
9
10
|
skillsDir: (home) => path.join(home, '.gemini', 'antigravity', 'skills'),
|
|
10
11
|
viepilotDir: (home) => path.join(home, '.gemini', 'antigravity', 'viepilot'),
|
|
12
|
+
// Phase 131 (FEAT-021): project-level install path (Antigravity v1 uses .agents/skills/)
|
|
13
|
+
projectSkillsDir: '.agents/skills',
|
|
11
14
|
// {envToolDir} in SKILL.md files resolves to this value at install time (ENH-035)
|
|
12
15
|
executionContextBase: '.gemini/antigravity/viepilot',
|
|
13
16
|
// Post-install hint shown in "Next actions" after viepilot install
|
|
14
17
|
postInstallHint: 'Open project and run /vp-status',
|
|
18
|
+
// Phase 131 (FEAT-021): Gemini CLI was deprecated June 18, 2026
|
|
19
|
+
deprecationNotice: '⚠️ Gemini CLI was deprecated June 18, 2026. This installs for Antigravity CLI (the successor). Skill path: .agents/skills/ (project) or ~/.gemini/antigravity/skills/ (global).',
|
|
15
20
|
hooks: {
|
|
16
21
|
configFile: null, // Antigravity has no programmatic hooks system
|
|
17
22
|
schema: 'antigravity',
|
|
18
|
-
supportedEvents: []
|
|
23
|
+
supportedEvents: ['before_tool', 'after_file_edit', 'session_start']
|
|
19
24
|
},
|
|
20
25
|
installSubdirs: [
|
|
21
26
|
'workflows',
|
|
@@ -30,6 +35,7 @@ module.exports = {
|
|
|
30
35
|
isAvailable: (home) => {
|
|
31
36
|
const h = home || os.homedir();
|
|
32
37
|
return fs.existsSync(path.join(h, '.gemini', 'antigravity'))
|
|
33
|
-
|| fs.existsSync(path.join(h, '.antigravity'))
|
|
38
|
+
|| fs.existsSync(path.join(h, '.antigravity'))
|
|
39
|
+
|| fs.existsSync(path.join(process.cwd(), '.agents'));
|
|
34
40
|
}
|
|
35
41
|
};
|
|
@@ -35,6 +35,10 @@ module.exports = {
|
|
|
35
35
|
'ui-components',
|
|
36
36
|
'agents'
|
|
37
37
|
],
|
|
38
|
+
// Phase 130 (FEAT-021): native Claude Code subagent definitions (.claude/agents/)
|
|
39
|
+
// Installed to ~/.claude/agents/ (Claude Code's native agent directory)
|
|
40
|
+
claudeAgentsDir: (home) => path.join(home, '.claude', 'agents'),
|
|
41
|
+
claudeAgentsSrc: 'agents/claude-code',
|
|
38
42
|
// Detection: is this platform available on the current machine?
|
|
39
43
|
isAvailable: (home) => {
|
|
40
44
|
const h = home || os.homedir();
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_BASE_URL = 'http://localhost:3000';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolve base URL for browser audit.
|
|
10
|
+
* Priority: options.baseUrl → config.json audit.baseUrl → DEFAULT_BASE_URL
|
|
11
|
+
* @param {object} options - { baseUrl?: string }
|
|
12
|
+
* @param {string} projectRoot
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
function resolveBaseUrl(options, projectRoot) {
|
|
16
|
+
if (options && options.baseUrl) return options.baseUrl;
|
|
17
|
+
try {
|
|
18
|
+
const configPath = path.join(projectRoot, '.viepilot', 'config.json');
|
|
19
|
+
if (fs.existsSync(configPath)) {
|
|
20
|
+
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
21
|
+
if (cfg.audit && cfg.audit.baseUrl) return cfg.audit.baseUrl;
|
|
22
|
+
}
|
|
23
|
+
} catch { /* config missing or malformed — use default */ }
|
|
24
|
+
return DEFAULT_BASE_URL;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Write a Markdown audit report to .viepilot/audit/visual-report-{timestamp}.md
|
|
29
|
+
* @param {object} report - structured audit report from browser-audit-agent
|
|
30
|
+
* @param {string} projectRoot
|
|
31
|
+
* @returns {string} absolute path to report file
|
|
32
|
+
*/
|
|
33
|
+
function writeAuditReport(report, projectRoot) {
|
|
34
|
+
const auditDir = path.join(projectRoot, '.viepilot', 'audit');
|
|
35
|
+
fs.mkdirSync(auditDir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
const timestamp = Date.now();
|
|
38
|
+
const reportPath = path.join(auditDir, `visual-report-${timestamp}.md`);
|
|
39
|
+
|
|
40
|
+
const routes = report.routes || [];
|
|
41
|
+
const routeTable = routes.map(r => {
|
|
42
|
+
const status = r.status === 'ok' ? '✅ ok' : `❌ ${r.status}`;
|
|
43
|
+
const issues = (r.errors && r.errors.length > 0) ? r.errors.join(', ') : '—';
|
|
44
|
+
return `| ${r.url} | ${status} | ${issues} |`;
|
|
45
|
+
}).join('\n');
|
|
46
|
+
|
|
47
|
+
const accessibilitySection = routes
|
|
48
|
+
.filter(r => r.accessibility_issues && r.accessibility_issues.length > 0)
|
|
49
|
+
.map(r => `### ${r.url}\n${r.accessibility_issues.map(i => `- **${i.type}**: ${i.description}`).join('\n')}`)
|
|
50
|
+
.join('\n\n') || '_No accessibility issues found._';
|
|
51
|
+
|
|
52
|
+
const screenshotSection = routes
|
|
53
|
+
.filter(r => r.screenshot)
|
|
54
|
+
.map(r => `- \`${r.url}\`: \`${r.screenshot}\``)
|
|
55
|
+
.join('\n') || '_No screenshots captured._';
|
|
56
|
+
|
|
57
|
+
const content = `# Browser Audit Report — ${new Date(timestamp).toISOString()}
|
|
58
|
+
|
|
59
|
+
**Base URL**: ${report.baseUrl || DEFAULT_BASE_URL}
|
|
60
|
+
**Routes checked**: ${routes.length}
|
|
61
|
+
**Op**: ${report.op || 'audit_routes'}
|
|
62
|
+
|
|
63
|
+
## Routes
|
|
64
|
+
|
|
65
|
+
| Route | Status | Issues |
|
|
66
|
+
|-------|--------|--------|
|
|
67
|
+
${routeTable}
|
|
68
|
+
|
|
69
|
+
## Accessibility Issues
|
|
70
|
+
|
|
71
|
+
${accessibilitySection}
|
|
72
|
+
|
|
73
|
+
## Screenshots
|
|
74
|
+
|
|
75
|
+
${screenshotSection}
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
fs.writeFileSync(reportPath, content, 'utf8');
|
|
79
|
+
return reportPath;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Run browser audit by dispatching browser-audit-agent (CC adapter only).
|
|
84
|
+
* On non-CC: throws with install instructions.
|
|
85
|
+
* @param {object} options - { baseUrl?, routes?, op?, updateBaseline? }
|
|
86
|
+
* @param {string} projectRoot
|
|
87
|
+
* @returns {Promise<object>} - report object
|
|
88
|
+
*/
|
|
89
|
+
async function runBrowserAudit(options, projectRoot) {
|
|
90
|
+
const baseUrl = resolveBaseUrl(options, projectRoot);
|
|
91
|
+
const op = (options && options.op) || 'audit_routes';
|
|
92
|
+
|
|
93
|
+
// CC adapter: Agent dispatch is handled by vp-audit SKILL.md (orchestrator layer).
|
|
94
|
+
// This module is for direct Node callers — emit clear error.
|
|
95
|
+
throw new Error(
|
|
96
|
+
`Browser audit requires Claude Code with the agent-browser skill.\n` +
|
|
97
|
+
`Install: npx skills add vercel-labs/agent-browser\n` +
|
|
98
|
+
`Then run: /vp-audit --visual --browser ${baseUrl}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = { runBrowserAudit, resolveBaseUrl, writeAuditReport, DEFAULT_BASE_URL };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const URL_PATTERNS = {
|
|
4
|
+
'google-sheets': /docs\.google\.com\/spreadsheets/,
|
|
5
|
+
'github-issues': /github\.com\/[^/]+\/[^/]+\/issues/,
|
|
6
|
+
'jira': /atlassian\.net\/browse\/[A-Z]+-\d+|atlassian\.net\/jira\/software\/projects/,
|
|
7
|
+
'trello': /trello\.com\/[bc]\//,
|
|
8
|
+
'notion': /notion\.so\/[a-f0-9]{8,}/,
|
|
9
|
+
'sharepoint-xlsx': /sharepoint\.com\/:[a-z]:\/[a-z]\//,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Detect source type from URL string.
|
|
14
|
+
* @param {string} url
|
|
15
|
+
* @returns {'google-sheets'|'github-issues'|'jira'|'trello'|'notion'|'sharepoint-xlsx'|'generic-table'}
|
|
16
|
+
*/
|
|
17
|
+
function detectUrlType(url) {
|
|
18
|
+
if (!url || typeof url !== 'string') return 'generic-table';
|
|
19
|
+
for (const [type, pattern] of Object.entries(URL_PATTERNS)) {
|
|
20
|
+
if (pattern.test(url)) return type;
|
|
21
|
+
}
|
|
22
|
+
return 'generic-table';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read ticket rows from a public URL via browser-intake-agent (CC adapter only).
|
|
27
|
+
* Non-CC adapters receive a clear unsupported error.
|
|
28
|
+
* @param {object} channel - channel config with `url` field
|
|
29
|
+
* @param {string} projectRoot - absolute path to project root
|
|
30
|
+
* @returns {Promise<Array<{title,description,labels,priority,status}>>}
|
|
31
|
+
*/
|
|
32
|
+
async function readBrowserUrl(channel, projectRoot) {
|
|
33
|
+
if (!channel || !channel.url) {
|
|
34
|
+
throw new Error('browser channel requires a "url" field in channel config');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const sourceType = detectUrlType(channel.url);
|
|
38
|
+
|
|
39
|
+
// CC adapter: Agent dispatch is handled by vp-intake SKILL.md (orchestrator layer).
|
|
40
|
+
// This module is used by non-CC adapters or direct Node callers — emit clear error.
|
|
41
|
+
throw new Error(
|
|
42
|
+
`Browser channel (${sourceType}) requires Claude Code with the agent-browser skill.\n` +
|
|
43
|
+
`Install: npx skills add vercel-labs/agent-browser\n` +
|
|
44
|
+
`Then run vp-intake from Claude Code.`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check whether a URL appears to be a publicly accessible intake source.
|
|
50
|
+
* Does not make network requests — pattern-match only.
|
|
51
|
+
* @param {string} url
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function isKnownPublicSource(url) {
|
|
55
|
+
return detectUrlType(url) !== 'generic-table';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = { readBrowserUrl, detectUrlType, isKnownPublicSource, URL_PATTERNS };
|
|
@@ -186,7 +186,7 @@ async function readViaGraphApi(channel, projectRoot) {
|
|
|
186
186
|
const response = await httpsGet(url, { Authorization: `Bearer ${token}` });
|
|
187
187
|
|
|
188
188
|
if (!response.values || response.values.length === 0) return [];
|
|
189
|
-
return parseRowsWithColumnMap(response.values, channel.column_map);
|
|
189
|
+
return parseRowsWithColumnMap(response.values, channel.column_map, channel.id);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
// ─── SharePoint sharing link (anonymous WOPI) ─────────────────────────────────
|
|
@@ -291,18 +291,64 @@ async function readViaSharingLink(channel) {
|
|
|
291
291
|
const rows = parseXlsxBuffer(xlsxBuffer, channel.sheet_name || null);
|
|
292
292
|
|
|
293
293
|
if (!rows || rows.length === 0) return [];
|
|
294
|
-
return parseRowsWithColumnMap(rows, channel.column_map);
|
|
294
|
+
return parseRowsWithColumnMap(rows, channel.column_map, channel.id);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ─── Vietnamese/English header → field name auto-detection ────────────────────
|
|
298
|
+
|
|
299
|
+
const HEADER_ALIASES = {
|
|
300
|
+
id: ['stt', 'id', 'no', '#', 'mã', 'ma'],
|
|
301
|
+
title: ['title', 'tiêu đề', 'tieu de', 'tên', 'ten', 'màn hình', 'man hinh', 'tên module', 'tên testcase', 'summary'],
|
|
302
|
+
description: ['mô tả lỗi', 'mo ta loi', 'description', 'mô tả', 'mo ta', 'chi tiết', 'chi tiet', 'detail', 'nội dung', 'noi dung'],
|
|
303
|
+
status: ['status', 'trạng thái', 'trang thai', 'kết quả', 'ket qua', 'result', 'p/f/n'],
|
|
304
|
+
reporter: ['reporter', 'người báo', 'nguoi bao', 'tester', 'người tạo', 'nguoi tao'],
|
|
305
|
+
priority: ['priority', 'mức độ', 'muc do', 'độ ưu tiên', 'severity'],
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
function autoDetectColumnMap(headerRow) {
|
|
309
|
+
const map = {};
|
|
310
|
+
headerRow.forEach((cell, idx) => {
|
|
311
|
+
const normalized = String(cell).toLowerCase().trim();
|
|
312
|
+
if (!normalized) return;
|
|
313
|
+
for (const [field, aliases] of Object.entries(HEADER_ALIASES)) {
|
|
314
|
+
if (!map[field] && aliases.some(a => normalized.includes(a))) {
|
|
315
|
+
// Convert 0-based index to Excel letter (A=0, B=1, ...)
|
|
316
|
+
let col = '';
|
|
317
|
+
let n = idx;
|
|
318
|
+
do { col = String.fromCharCode(65 + (n % 26)) + col; n = Math.floor(n / 26) - 1; } while (n >= 0);
|
|
319
|
+
map[field] = col;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
return map;
|
|
295
324
|
}
|
|
296
325
|
|
|
297
326
|
// ─── Common row → ticket mapper ───────────────────────────────────────────────
|
|
298
327
|
|
|
299
|
-
function parseRowsWithColumnMap(values, colMap) {
|
|
328
|
+
function parseRowsWithColumnMap(values, colMap, channelId) {
|
|
329
|
+
// Find first non-empty row to use as auto-detect source if needed
|
|
330
|
+
const firstNonEmpty = values.find(r => r.some(c => c !== ''));
|
|
331
|
+
|
|
332
|
+
// Auto-detect or fall back to raw if still missing
|
|
333
|
+
const resolvedMap = (colMap && Object.keys(colMap).length > 0)
|
|
334
|
+
? colMap
|
|
335
|
+
: (firstNonEmpty ? autoDetectColumnMap(firstNonEmpty) : {});
|
|
336
|
+
|
|
337
|
+
if (!colMap || Object.keys(colMap).length === 0) {
|
|
338
|
+
process.stderr.write(
|
|
339
|
+
`[vp-intake] No column_map configured. Auto-detected: ${JSON.stringify(resolvedMap)}. ` +
|
|
340
|
+
`Header row: ${JSON.stringify(firstNonEmpty || [])}\n`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
300
344
|
const tickets = [];
|
|
345
|
+
// Skip header row(s): if auto-detected, skip until first row that has a non-header value
|
|
346
|
+
const startRow = (colMap && Object.keys(colMap).length > 0) ? 1 : values.findIndex(r => r.some(c => c !== '')) + 1;
|
|
301
347
|
|
|
302
|
-
for (let i =
|
|
348
|
+
for (let i = startRow; i < values.length; i++) {
|
|
303
349
|
const row = values[i];
|
|
304
350
|
const get = (field) => {
|
|
305
|
-
const col =
|
|
351
|
+
const col = resolvedMap[field];
|
|
306
352
|
if (!col) return '';
|
|
307
353
|
const idx = colLetterToIndex(col);
|
|
308
354
|
return row[idx] !== undefined ? String(row[idx]) : '';
|
|
@@ -316,7 +362,7 @@ function parseRowsWithColumnMap(values, colMap) {
|
|
|
316
362
|
date: get('date'),
|
|
317
363
|
status: get('status'),
|
|
318
364
|
_source_row: i,
|
|
319
|
-
_channel_id:
|
|
365
|
+
_channel_id: channelId,
|
|
320
366
|
};
|
|
321
367
|
|
|
322
368
|
if (!ticket.title && !ticket.description) continue;
|
|
@@ -362,4 +408,6 @@ module.exports = {
|
|
|
362
408
|
isSharingLink,
|
|
363
409
|
resolveSharePointDownloadUrl,
|
|
364
410
|
parseXlsxBuffer,
|
|
411
|
+
autoDetectColumnMap,
|
|
412
|
+
HEADER_ALIASES,
|
|
365
413
|
};
|