@cyber-dash-tech/revela 0.1.0
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/LICENSE +21 -0
- package/README.md +239 -0
- package/README.zh-CN.md +270 -0
- package/designs/default/DESIGN.md +1100 -0
- package/designs/editorial-ribbon/DESIGN.md +1092 -0
- package/designs/minimal/DESIGN.md +1079 -0
- package/domains/consulting/INDUSTRY.md +230 -0
- package/domains/deeptech-investment/INDUSTRY.md +160 -0
- package/domains/general/INDUSTRY.md +6 -0
- package/index.ts +1 -0
- package/lib/agents/research-prompt.ts +129 -0
- package/lib/commands/designs.ts +59 -0
- package/lib/commands/disable.ts +14 -0
- package/lib/commands/domains.ts +59 -0
- package/lib/commands/enable.ts +48 -0
- package/lib/commands/help.ts +35 -0
- package/lib/config.ts +65 -0
- package/lib/ctx.ts +27 -0
- package/lib/design/designs.ts +389 -0
- package/lib/domain/domains.ts +258 -0
- package/lib/frontmatter.ts +63 -0
- package/lib/log.ts +35 -0
- package/lib/prompt-builder.ts +194 -0
- package/lib/qa/checks.ts +594 -0
- package/lib/qa/index.ts +38 -0
- package/lib/qa/measure.ts +287 -0
- package/lib/read-hooks/extractors/docx.ts +16 -0
- package/lib/read-hooks/extractors/pdf.ts +19 -0
- package/lib/read-hooks/extractors/pptx.ts +53 -0
- package/lib/read-hooks/extractors/xlsx.ts +81 -0
- package/lib/read-hooks/image/compress.ts +36 -0
- package/lib/read-hooks/index.ts +12 -0
- package/lib/read-hooks/post-read.ts +74 -0
- package/lib/read-hooks/pre-read.ts +51 -0
- package/package.json +65 -0
- package/plugin.ts +365 -0
- package/skill/SKILL.md +676 -0
- package/tools/designs.ts +126 -0
- package/tools/domains.ts +73 -0
- package/tools/qa.ts +61 -0
- package/tools/research-save.ts +96 -0
- package/tools/workspace-scan.ts +154 -0
package/tools/designs.ts
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import {
|
|
3
|
+
listDesigns,
|
|
4
|
+
activeDesign,
|
|
5
|
+
activateDesign,
|
|
6
|
+
installDesign,
|
|
7
|
+
removeDesign,
|
|
8
|
+
parseDesignSections,
|
|
9
|
+
generateComponentIndex,
|
|
10
|
+
getDesignSection,
|
|
11
|
+
getDesignComponent,
|
|
12
|
+
} from "../lib/design/designs"
|
|
13
|
+
import { buildPrompt } from "../lib/prompt-builder"
|
|
14
|
+
import { existsSync, readFileSync } from "fs"
|
|
15
|
+
import { join } from "path"
|
|
16
|
+
import { DESIGNS_DIR } from "../lib/config"
|
|
17
|
+
import { parseFrontmatter } from "../lib/frontmatter"
|
|
18
|
+
|
|
19
|
+
export default tool({
|
|
20
|
+
description:
|
|
21
|
+
"Manage revela visual design templates. " +
|
|
22
|
+
"Use action 'list' to show all installed designs with names and descriptions. " +
|
|
23
|
+
"Use action 'activate' to switch to a different design (requires name). " +
|
|
24
|
+
"Use action 'install' to add a new design from a URL, local path, or github:user/repo shorthand (requires source). " +
|
|
25
|
+
"Use action 'remove' to uninstall a design (requires name). " +
|
|
26
|
+
"Use action 'read' to fetch on-demand design content: pass component (comma-separated names) to get full CSS/HTML for specific components, or section ('charts' | 'guide' | 'global' | 'layouts' | 'components') to get an entire section. Pass neither to get the Component Index table. " +
|
|
27
|
+
"After activating a new design, the system prompt is automatically rebuilt.",
|
|
28
|
+
args: {
|
|
29
|
+
action: tool.schema
|
|
30
|
+
.enum(["list", "activate", "install", "remove", "read"])
|
|
31
|
+
.describe("Operation to perform"),
|
|
32
|
+
name: tool.schema
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe("Design name — required for activate and remove; optional for read (defaults to active design)"),
|
|
36
|
+
source: tool.schema
|
|
37
|
+
.string()
|
|
38
|
+
.optional()
|
|
39
|
+
.describe(
|
|
40
|
+
"Install source — URL, local path, github:user/repo. Required for install."
|
|
41
|
+
),
|
|
42
|
+
component: tool.schema
|
|
43
|
+
.string()
|
|
44
|
+
.optional()
|
|
45
|
+
.describe(
|
|
46
|
+
"For action 'read': comma-separated component name(s) to fetch (e.g. 'card', 'card,stat-card')"
|
|
47
|
+
),
|
|
48
|
+
section: tool.schema
|
|
49
|
+
.string()
|
|
50
|
+
.optional()
|
|
51
|
+
.describe(
|
|
52
|
+
"For action 'read': section name to fetch — 'charts', 'guide', 'global', 'layouts', or 'components'"
|
|
53
|
+
),
|
|
54
|
+
},
|
|
55
|
+
async execute(args) {
|
|
56
|
+
try {
|
|
57
|
+
switch (args.action) {
|
|
58
|
+
case "list": {
|
|
59
|
+
const designs = listDesigns()
|
|
60
|
+
const current = activeDesign()
|
|
61
|
+
return JSON.stringify(
|
|
62
|
+
designs.map((d) => ({
|
|
63
|
+
name: d.name,
|
|
64
|
+
description: d.description,
|
|
65
|
+
author: d.author,
|
|
66
|
+
version: d.version,
|
|
67
|
+
active: d.name === current,
|
|
68
|
+
})),
|
|
69
|
+
null,
|
|
70
|
+
2,
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
case "activate": {
|
|
74
|
+
if (!args.name) return JSON.stringify({ error: "name is required for activate" })
|
|
75
|
+
activateDesign(args.name)
|
|
76
|
+
buildPrompt()
|
|
77
|
+
return JSON.stringify({ ok: true })
|
|
78
|
+
}
|
|
79
|
+
case "install": {
|
|
80
|
+
if (!args.source) return JSON.stringify({ error: "source is required for install" })
|
|
81
|
+
const installed = await installDesign(args.source, args.name)
|
|
82
|
+
return JSON.stringify({ ok: true, name: installed })
|
|
83
|
+
}
|
|
84
|
+
case "remove": {
|
|
85
|
+
if (!args.name) return JSON.stringify({ error: "name is required for remove" })
|
|
86
|
+
removeDesign(args.name)
|
|
87
|
+
return JSON.stringify({ ok: true })
|
|
88
|
+
}
|
|
89
|
+
case "read": {
|
|
90
|
+
const designName = args.name || activeDesign()
|
|
91
|
+
|
|
92
|
+
// Read raw body to check for markers
|
|
93
|
+
const mdPath = join(DESIGNS_DIR, designName, "DESIGN.md")
|
|
94
|
+
if (!existsSync(mdPath)) {
|
|
95
|
+
return JSON.stringify({ error: `Design '${designName}' is not installed` })
|
|
96
|
+
}
|
|
97
|
+
const raw = readFileSync(mdPath, "utf-8")
|
|
98
|
+
const { body } = parseFrontmatter(raw)
|
|
99
|
+
const { components, hasMarkers } = parseDesignSections(body)
|
|
100
|
+
|
|
101
|
+
if (!hasMarkers) {
|
|
102
|
+
// No markers — return full body
|
|
103
|
+
return body
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Specific component(s) requested
|
|
107
|
+
if (args.component) {
|
|
108
|
+
return getDesignComponent(args.component, designName)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Specific section requested
|
|
112
|
+
if (args.section) {
|
|
113
|
+
return getDesignSection(args.section, designName)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Default: return Component Index
|
|
117
|
+
return generateComponentIndex(components)
|
|
118
|
+
}
|
|
119
|
+
default:
|
|
120
|
+
return JSON.stringify({ error: `Unknown action: ${args.action}` })
|
|
121
|
+
}
|
|
122
|
+
} catch (e: any) {
|
|
123
|
+
return JSON.stringify({ error: e.message || String(e) })
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
})
|
package/tools/domains.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import {
|
|
3
|
+
listDomains,
|
|
4
|
+
activeDomain,
|
|
5
|
+
activateDomain,
|
|
6
|
+
installDomain,
|
|
7
|
+
removeDomain,
|
|
8
|
+
} from "../lib/domain/domains"
|
|
9
|
+
import { buildPrompt } from "../lib/prompt-builder"
|
|
10
|
+
|
|
11
|
+
export default tool({
|
|
12
|
+
description:
|
|
13
|
+
"Manage revela domain definitions (industry/topic specializations). " +
|
|
14
|
+
"Use action 'list' to show all installed domains with names and descriptions. " +
|
|
15
|
+
"Use action 'activate' to switch to a different domain (requires name). " +
|
|
16
|
+
"Use action 'install' to add a new domain from a URL, local path, or github:user/repo shorthand (requires source). " +
|
|
17
|
+
"Use action 'remove' to uninstall a domain — 'general' cannot be removed (requires name). " +
|
|
18
|
+
"After activating a new domain, the system prompt is automatically rebuilt.",
|
|
19
|
+
args: {
|
|
20
|
+
action: tool.schema
|
|
21
|
+
.enum(["list", "activate", "install", "remove"])
|
|
22
|
+
.describe("Operation to perform"),
|
|
23
|
+
name: tool.schema
|
|
24
|
+
.string()
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Domain name — required for activate and remove"),
|
|
27
|
+
source: tool.schema
|
|
28
|
+
.string()
|
|
29
|
+
.optional()
|
|
30
|
+
.describe("Install source — URL, local path, github:user/repo. Required for install."),
|
|
31
|
+
},
|
|
32
|
+
async execute(args) {
|
|
33
|
+
try {
|
|
34
|
+
switch (args.action) {
|
|
35
|
+
case "list": {
|
|
36
|
+
const domains = listDomains()
|
|
37
|
+
const current = activeDomain()
|
|
38
|
+
return JSON.stringify(
|
|
39
|
+
domains.map((d) => ({
|
|
40
|
+
name: d.name,
|
|
41
|
+
description: d.description,
|
|
42
|
+
author: d.author,
|
|
43
|
+
version: d.version,
|
|
44
|
+
active: d.name === current,
|
|
45
|
+
})),
|
|
46
|
+
null,
|
|
47
|
+
2,
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
case "activate": {
|
|
51
|
+
if (!args.name) return JSON.stringify({ error: "name is required for activate" })
|
|
52
|
+
activateDomain(args.name)
|
|
53
|
+
buildPrompt()
|
|
54
|
+
return JSON.stringify({ ok: true })
|
|
55
|
+
}
|
|
56
|
+
case "install": {
|
|
57
|
+
if (!args.source) return JSON.stringify({ error: "source is required for install" })
|
|
58
|
+
const installed = await installDomain(args.source, args.name)
|
|
59
|
+
return JSON.stringify({ ok: true, name: installed })
|
|
60
|
+
}
|
|
61
|
+
case "remove": {
|
|
62
|
+
if (!args.name) return JSON.stringify({ error: "name is required for remove" })
|
|
63
|
+
removeDomain(args.name)
|
|
64
|
+
return JSON.stringify({ ok: true })
|
|
65
|
+
}
|
|
66
|
+
default:
|
|
67
|
+
return JSON.stringify({ error: `Unknown action: ${args.action}` })
|
|
68
|
+
}
|
|
69
|
+
} catch (e: any) {
|
|
70
|
+
return JSON.stringify({ error: e.message || String(e) })
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
})
|
package/tools/qa.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tools/qa.ts
|
|
3
|
+
*
|
|
4
|
+
* revela-qa — Layout quality assurance tool for generated slide HTML files.
|
|
5
|
+
*
|
|
6
|
+
* Exposed to the LLM so it can run layout checks after writing a slides file.
|
|
7
|
+
* Also called automatically by the tool.execute.after hook in plugin.ts
|
|
8
|
+
* when the LLM writes a file matching slides/*.html.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { tool } from "@opencode-ai/plugin"
|
|
12
|
+
import { resolve } from "path"
|
|
13
|
+
import { existsSync } from "fs"
|
|
14
|
+
import { runQA, formatReport } from "../lib/qa"
|
|
15
|
+
|
|
16
|
+
export default tool({
|
|
17
|
+
description:
|
|
18
|
+
"Run layout quality checks on a generated slide HTML file. " +
|
|
19
|
+
"Opens the file in a headless browser and measures actual rendered geometry. " +
|
|
20
|
+
"Checks for: canvas underfill (too much empty space), bottom whitespace, " +
|
|
21
|
+
"left-right column asymmetry, element overflow, and card height variance. " +
|
|
22
|
+
"Returns a structured report with specific issues and fix instructions. " +
|
|
23
|
+
"Call this after writing or editing any slides/*.html file to verify layout quality.",
|
|
24
|
+
args: {
|
|
25
|
+
file: tool.schema
|
|
26
|
+
.string()
|
|
27
|
+
.describe(
|
|
28
|
+
"Path to the HTML slide file to check. " +
|
|
29
|
+
"Can be absolute or relative to the current working directory."
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
async execute({ file }, { directory }) {
|
|
33
|
+
// Resolve path relative to working directory
|
|
34
|
+
const filePath = resolve(directory || process.cwd(), file)
|
|
35
|
+
|
|
36
|
+
if (!existsSync(filePath)) {
|
|
37
|
+
return `Error: File not found: ${filePath}`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!filePath.endsWith(".html")) {
|
|
41
|
+
return `Error: File must be an HTML file: ${filePath}`
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const report = await runQA(filePath)
|
|
46
|
+
const formatted = formatReport(report)
|
|
47
|
+
|
|
48
|
+
// Prepend a compact JSON summary for programmatic use if needed
|
|
49
|
+
const jsonSummary = JSON.stringify({
|
|
50
|
+
totalIssues: report.totalIssues,
|
|
51
|
+
errors: report.errorCount,
|
|
52
|
+
warnings: report.warningCount,
|
|
53
|
+
slidesWithIssues: report.slides.filter((s) => s.issues.length > 0).map((s) => s.index + 1),
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return `<!-- QA Summary: ${jsonSummary} -->\n\n${formatted}`
|
|
57
|
+
} catch (err: any) {
|
|
58
|
+
return `Error running layout QA: ${err?.message ?? String(err)}\n\nMake sure Chrome is installed at /Applications/Google Chrome.app`
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
})
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs"
|
|
3
|
+
import { join } from "path"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format today's date as YYYY-MM-DD
|
|
7
|
+
*/
|
|
8
|
+
function today(): string {
|
|
9
|
+
return new Date().toISOString().slice(0, 10)
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Sanitize a slug: lowercase, alphanumeric + hyphens only.
|
|
14
|
+
*/
|
|
15
|
+
function slugify(s: string): string {
|
|
16
|
+
return s
|
|
17
|
+
.toLowerCase()
|
|
18
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
19
|
+
.replace(/^-+|-+$/g, "")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build YAML frontmatter string.
|
|
24
|
+
*/
|
|
25
|
+
function buildFrontmatter(topic: string, axis: string, sources: string[]): string {
|
|
26
|
+
const lines = [
|
|
27
|
+
"---",
|
|
28
|
+
`topic: ${topic}`,
|
|
29
|
+
`axis: ${axis}`,
|
|
30
|
+
`date: ${today()}`,
|
|
31
|
+
]
|
|
32
|
+
if (sources.length > 0) {
|
|
33
|
+
lines.push("sources:")
|
|
34
|
+
for (const s of sources) {
|
|
35
|
+
lines.push(` - "${s.replace(/"/g, '\\"')}"`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
lines.push("---")
|
|
39
|
+
return lines.join("\n")
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export default tool({
|
|
43
|
+
description:
|
|
44
|
+
"Save a research findings file to the workspace researches/ directory. " +
|
|
45
|
+
"Creates researches/{topic}/{filename}.md with YAML frontmatter. " +
|
|
46
|
+
"Each research axis gets its own file (e.g. 'market-data', 'catl-profile'). " +
|
|
47
|
+
"Content should use ## Data / ## Cases / ## Images / ## Gaps sections.",
|
|
48
|
+
args: {
|
|
49
|
+
topic: tool.schema
|
|
50
|
+
.string()
|
|
51
|
+
.describe(
|
|
52
|
+
"Topic slug in kebab-case, e.g. 'ev-battery-market' or 'saas-competitive-analysis'. " +
|
|
53
|
+
"All files for the same presentation share the same topic slug.",
|
|
54
|
+
),
|
|
55
|
+
filename: tool.schema
|
|
56
|
+
.string()
|
|
57
|
+
.describe(
|
|
58
|
+
"Axis name without extension, e.g. 'market-data', 'catl-profile', 'tech-trends'. " +
|
|
59
|
+
"Each parallel research agent uses a unique axis name.",
|
|
60
|
+
),
|
|
61
|
+
content: tool.schema
|
|
62
|
+
.string()
|
|
63
|
+
.describe(
|
|
64
|
+
"Structured markdown findings. Use these sections (omit empty ones):\n" +
|
|
65
|
+
"## Data — key stats and data points, each with [Source: url]\n" +
|
|
66
|
+
"## Cases — company/entity profiles, 1-2 sentences each with [Source: url]\n" +
|
|
67
|
+
"## Images — image URLs: '{description}: {url} | Alt: {text} | Use: logo|screenshot|portrait'\n" +
|
|
68
|
+
"## Gaps — topics not found or insufficiently covered",
|
|
69
|
+
),
|
|
70
|
+
sources: tool.schema
|
|
71
|
+
.array(tool.schema.string())
|
|
72
|
+
.optional()
|
|
73
|
+
.describe("Source URLs or filenames for YAML frontmatter, e.g. ['https://example.com/report', 'data.xlsx']"),
|
|
74
|
+
},
|
|
75
|
+
async execute(args, context) {
|
|
76
|
+
try {
|
|
77
|
+
const topicSlug = slugify(args.topic || "research")
|
|
78
|
+
const fileSlug = slugify(args.filename || "findings")
|
|
79
|
+
const workspaceDir = context.directory ?? process.cwd()
|
|
80
|
+
const topicDir = join(workspaceDir, "researches", topicSlug)
|
|
81
|
+
|
|
82
|
+
mkdirSync(topicDir, { recursive: true })
|
|
83
|
+
|
|
84
|
+
const frontmatter = buildFrontmatter(args.topic, fileSlug, args.sources ?? [])
|
|
85
|
+
const fileContent = `${frontmatter}\n\n${args.content ?? ""}\n`
|
|
86
|
+
const filePath = join(topicDir, `${fileSlug}.md`)
|
|
87
|
+
const relPath = `researches/${topicSlug}/${fileSlug}.md`
|
|
88
|
+
|
|
89
|
+
writeFileSync(filePath, fileContent, "utf-8")
|
|
90
|
+
|
|
91
|
+
return JSON.stringify({ ok: true, path: relPath })
|
|
92
|
+
} catch (e: any) {
|
|
93
|
+
return JSON.stringify({ error: e.message || String(e) })
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
})
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin"
|
|
2
|
+
import { readdirSync, statSync, existsSync } from "fs"
|
|
3
|
+
import { join, relative, extname } from "path"
|
|
4
|
+
|
|
5
|
+
const DOC_EXTENSIONS = new Set([
|
|
6
|
+
".pdf", ".docx", ".doc", ".xlsx", ".xls",
|
|
7
|
+
".pptx", ".ppt", ".csv", ".md", ".txt",
|
|
8
|
+
])
|
|
9
|
+
|
|
10
|
+
// Directories to exclude from scanning
|
|
11
|
+
const EXCLUDE_DIRS = new Set([
|
|
12
|
+
"node_modules", ".git", "dist", ".opencode",
|
|
13
|
+
"researches", // Exclude revela's own research output
|
|
14
|
+
"designs", "domains", // Exclude revela plugin assets
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
type FileEntry = {
|
|
18
|
+
path: string
|
|
19
|
+
type: string
|
|
20
|
+
size: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Format bytes into a human-readable size string.
|
|
25
|
+
*/
|
|
26
|
+
function formatSize(bytes: number): string {
|
|
27
|
+
if (bytes < 1024) return `${bytes} B`
|
|
28
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
29
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Map extension to a friendly type label.
|
|
34
|
+
*/
|
|
35
|
+
function typeLabel(ext: string): string {
|
|
36
|
+
const map: Record<string, string> = {
|
|
37
|
+
".pdf": "PDF",
|
|
38
|
+
".docx": "Word",
|
|
39
|
+
".doc": "Word",
|
|
40
|
+
".xlsx": "Excel",
|
|
41
|
+
".xls": "Excel",
|
|
42
|
+
".pptx": "PowerPoint",
|
|
43
|
+
".ppt": "PowerPoint",
|
|
44
|
+
".csv": "CSV",
|
|
45
|
+
".md": "Markdown",
|
|
46
|
+
".txt": "Text",
|
|
47
|
+
}
|
|
48
|
+
return map[ext] ?? ext.slice(1).toUpperCase()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Recursively scan a directory for document files.
|
|
53
|
+
*/
|
|
54
|
+
function scanDir(dir: string, rootDir: string, results: FileEntry[], maxDepth: number, depth: number): void {
|
|
55
|
+
if (depth > maxDepth) return
|
|
56
|
+
if (!existsSync(dir)) return
|
|
57
|
+
|
|
58
|
+
let entries: string[]
|
|
59
|
+
try {
|
|
60
|
+
entries = readdirSync(dir)
|
|
61
|
+
} catch {
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const entry of entries) {
|
|
66
|
+
// Skip hidden files/dirs and excluded dirs
|
|
67
|
+
if (entry.startsWith(".")) continue
|
|
68
|
+
if (EXCLUDE_DIRS.has(entry)) continue
|
|
69
|
+
|
|
70
|
+
const fullPath = join(dir, entry)
|
|
71
|
+
let stat
|
|
72
|
+
try {
|
|
73
|
+
stat = statSync(fullPath)
|
|
74
|
+
} catch {
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (stat.isDirectory()) {
|
|
79
|
+
scanDir(fullPath, rootDir, results, maxDepth, depth + 1)
|
|
80
|
+
} else if (stat.isFile()) {
|
|
81
|
+
const ext = extname(entry).toLowerCase()
|
|
82
|
+
if (DOC_EXTENSIONS.has(ext)) {
|
|
83
|
+
results.push({
|
|
84
|
+
path: relative(rootDir, fullPath),
|
|
85
|
+
type: typeLabel(ext),
|
|
86
|
+
size: formatSize(stat.size),
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default tool({
|
|
94
|
+
description:
|
|
95
|
+
"Scan the current workspace for document and data files that can be used as research input. " +
|
|
96
|
+
"Returns a structured list of all found files with their type and size. " +
|
|
97
|
+
"Searches for: PDF, Word (docx/doc), Excel (xlsx/xls), PowerPoint (pptx/ppt), CSV, Markdown, and text files. " +
|
|
98
|
+
"Excludes node_modules, .git, dist, and the researches/ output directory. " +
|
|
99
|
+
"Use this as the first step before reading workspace documents.",
|
|
100
|
+
args: {
|
|
101
|
+
path: tool.schema
|
|
102
|
+
.string()
|
|
103
|
+
.optional()
|
|
104
|
+
.describe(
|
|
105
|
+
"Optional subdirectory to scan (relative to workspace root). " +
|
|
106
|
+
"If omitted, scans the entire workspace.",
|
|
107
|
+
),
|
|
108
|
+
max_depth: tool.schema
|
|
109
|
+
.number()
|
|
110
|
+
.optional()
|
|
111
|
+
.describe("Maximum directory depth to recurse. Defaults to 6."),
|
|
112
|
+
},
|
|
113
|
+
async execute(args, context) {
|
|
114
|
+
try {
|
|
115
|
+
const workspaceDir = context.directory ?? process.cwd()
|
|
116
|
+
const scanRoot = args.path ? join(workspaceDir, args.path) : workspaceDir
|
|
117
|
+
const maxDepth = args.max_depth ?? 6
|
|
118
|
+
|
|
119
|
+
if (!existsSync(scanRoot)) {
|
|
120
|
+
return JSON.stringify({ error: `Path not found: ${args.path}` })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const results: FileEntry[] = []
|
|
124
|
+
scanDir(scanRoot, workspaceDir, results, maxDepth, 0)
|
|
125
|
+
|
|
126
|
+
if (results.length === 0) {
|
|
127
|
+
return JSON.stringify({
|
|
128
|
+
found: 0,
|
|
129
|
+
message: "No document files found in workspace.",
|
|
130
|
+
files: [],
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Sort by type then path for readability
|
|
135
|
+
results.sort((a, b) => a.type.localeCompare(b.type) || a.path.localeCompare(b.path))
|
|
136
|
+
|
|
137
|
+
// Build markdown table
|
|
138
|
+
const tableRows = results.map((f) => `| ${f.path} | ${f.type} | ${f.size} |`)
|
|
139
|
+
const table = [
|
|
140
|
+
"| File | Type | Size |",
|
|
141
|
+
"|------|------|------|",
|
|
142
|
+
...tableRows,
|
|
143
|
+
].join("\n")
|
|
144
|
+
|
|
145
|
+
return JSON.stringify({
|
|
146
|
+
found: results.length,
|
|
147
|
+
table,
|
|
148
|
+
files: results,
|
|
149
|
+
})
|
|
150
|
+
} catch (e: any) {
|
|
151
|
+
return JSON.stringify({ error: e.message || String(e) })
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
})
|