@tanstack/intent 0.0.1
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 +66 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +327 -0
- package/dist/feedback-DKreHfB1.mjs +300 -0
- package/dist/feedback-FIUBOL0g.mjs +3 -0
- package/dist/index.d.mts +61 -0
- package/dist/index.mjs +8 -0
- package/dist/init-DEzzXm9j.mjs +3 -0
- package/dist/init-DNxmjQfU.mjs +70 -0
- package/dist/intent-library.d.mts +1 -0
- package/dist/intent-library.mjs +123 -0
- package/dist/library-scanner-BrznE00j.mjs +111 -0
- package/dist/library-scanner.d.mts +16 -0
- package/dist/library-scanner.mjs +4 -0
- package/dist/scanner-BuWPDJ4P.mjs +4 -0
- package/dist/scanner-CpsJAHXT.mjs +147 -0
- package/dist/setup-CNGz26qL.mjs +116 -0
- package/dist/setup-N5dttGp_.d.mts +10 -0
- package/dist/setup.d.mts +2 -0
- package/dist/setup.mjs +3 -0
- package/dist/staleness-CnomT9Hm.mjs +72 -0
- package/dist/staleness-DyhsrqQ5.mjs +4 -0
- package/dist/types-kbQfN_is.d.mts +70 -0
- package/dist/utils-DjkEPBxu.mjs +39 -0
- package/meta/domain-discovery/SKILL.md +681 -0
- package/meta/generate-skill/SKILL.md +419 -0
- package/meta/skill-staleness-check/SKILL.md +282 -0
- package/meta/templates/oz/domain-discovery.md +53 -0
- package/meta/templates/oz/feedback-collection.md +69 -0
- package/meta/templates/oz/skill-update.md +47 -0
- package/meta/templates/oz/tree-generation.md +48 -0
- package/meta/templates/workflows/generate-skills-oz.yml +86 -0
- package/meta/templates/workflows/notify-playbooks.yml +52 -0
- package/meta/templates/workflows/update-skills-oz.yml +98 -0
- package/meta/templates/workflows/validate-skills.yml +52 -0
- package/meta/tree-generator/SKILL.md +859 -0
- package/package.json +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# @tanstack/intent
|
|
2
|
+
|
|
3
|
+
Ship compositional knowledge for AI coding agents alongside your npm packages.
|
|
4
|
+
|
|
5
|
+
Skills are npm packages of knowledge — encoding how tools work together, what patterns apply for which goals, and what to avoid. Skills travel with the tool via `npm update`, not the model's training cutoff.
|
|
6
|
+
|
|
7
|
+
`@tanstack/intent` is the toolkit for generating, discovering, and maintaining skills for your library.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pnpm add -D @tanstack/intent
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
### For library consumers
|
|
18
|
+
|
|
19
|
+
Set up intent discovery in your project's agent config files (CLAUDE.md, .cursorrules, etc.):
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
npx intent init
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
List available skills from installed packages:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx intent list
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### For library maintainers
|
|
32
|
+
|
|
33
|
+
Generate skills for your library using the guided scaffold workflow:
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
npx intent scaffold
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Validate your skill files:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx intent validate
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Copy CI and Oz workflow templates into your repo:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx intent setup
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## CLI Commands
|
|
52
|
+
|
|
53
|
+
| Command | Description |
|
|
54
|
+
| ------------------------ | ------------------------------------------------ |
|
|
55
|
+
| `intent init` | Inject intent discovery into agent config files |
|
|
56
|
+
| `intent list [--json]` | Discover intent-enabled packages |
|
|
57
|
+
| `intent meta` | List meta-skills for library maintainers |
|
|
58
|
+
| `intent scaffold` | Print the guided skill generation prompt |
|
|
59
|
+
| `intent validate [dir]` | Validate SKILL.md files |
|
|
60
|
+
| `intent setup` | Copy CI/Oz workflow templates |
|
|
61
|
+
| `intent stale [--json]` | Check skills for version drift |
|
|
62
|
+
| `intent feedback` | Submit skill feedback |
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
[MIT](./LICENSE)
|
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as parseFrontmatter, t as findSkillFiles } from "./utils-DjkEPBxu.mjs";
|
|
3
|
+
import { t as scanForIntents } from "./scanner-CpsJAHXT.mjs";
|
|
4
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
5
|
+
import { dirname, join, relative, sep } from "node:path";
|
|
6
|
+
import { parse } from "yaml";
|
|
7
|
+
import { spawnSync } from "node:child_process";
|
|
8
|
+
import { release } from "node:os";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
//#region src/cli.ts
|
|
12
|
+
function getMetaDir() {
|
|
13
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..", "meta");
|
|
14
|
+
}
|
|
15
|
+
async function cmdList(args) {
|
|
16
|
+
const jsonOutput = args.includes("--json");
|
|
17
|
+
let result;
|
|
18
|
+
try {
|
|
19
|
+
result = await scanForIntents();
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(err.message);
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
if (jsonOutput) {
|
|
25
|
+
console.log(JSON.stringify(result, null, 2));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (result.packages.length === 0) {
|
|
29
|
+
console.log("No intent-enabled packages found.");
|
|
30
|
+
if (result.warnings.length > 0) {
|
|
31
|
+
console.log(`\nWarnings:`);
|
|
32
|
+
for (const w of result.warnings) console.log(` ⚠ ${w}`);
|
|
33
|
+
}
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(`Intent-enabled packages (${result.packages.length} found):\n`);
|
|
37
|
+
for (const pkg of result.packages) {
|
|
38
|
+
const reqStr = pkg.intent.requires?.length ? ` (requires: ${pkg.intent.requires.join(", ")})` : "";
|
|
39
|
+
console.log(`${pkg.name} v${pkg.version}${reqStr}`);
|
|
40
|
+
for (const skill of pkg.skills) {
|
|
41
|
+
const desc = skill.description ? ` ${skill.description}` : "";
|
|
42
|
+
console.log(` ${skill.name.padEnd(32)}${desc}`);
|
|
43
|
+
}
|
|
44
|
+
console.log();
|
|
45
|
+
}
|
|
46
|
+
if (result.warnings.length > 0) {
|
|
47
|
+
console.log(`Warnings:`);
|
|
48
|
+
for (const w of result.warnings) console.log(` ⚠ ${w}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function cmdMeta() {
|
|
52
|
+
const metaDir = getMetaDir();
|
|
53
|
+
if (!existsSync(metaDir)) {
|
|
54
|
+
console.error("Meta-skills directory not found.");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
const entries = readdirSync(metaDir, { withFileTypes: true }).filter((e) => e.isDirectory()).filter((e) => existsSync(join(metaDir, e.name, "SKILL.md")));
|
|
58
|
+
if (entries.length === 0) {
|
|
59
|
+
console.log("No meta-skills found.");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
console.log("Meta-skills (for library maintainers):\n");
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
const fm = parseFrontmatter(join(metaDir, entry.name, "SKILL.md"));
|
|
65
|
+
let description = "";
|
|
66
|
+
if (typeof fm?.description === "string") description = fm.description.replace(/\s+/g, " ").trim();
|
|
67
|
+
const shortDesc = description.length > 60 ? description.slice(0, 57) + "..." : description;
|
|
68
|
+
console.log(` ${entry.name.padEnd(28)} ${shortDesc}`);
|
|
69
|
+
}
|
|
70
|
+
console.log(`\nUsage: load the SKILL.md into your AI agent conversation.`);
|
|
71
|
+
console.log(`Path: node_modules/@tanstack/intent/meta/<name>/SKILL.md`);
|
|
72
|
+
}
|
|
73
|
+
function cmdValidate(args) {
|
|
74
|
+
const targetDir = args[0] ?? "skills";
|
|
75
|
+
const skillsDir = join(process.cwd(), targetDir);
|
|
76
|
+
if (!existsSync(skillsDir)) {
|
|
77
|
+
console.error(`Skills directory not found: ${skillsDir}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
const errors = [];
|
|
81
|
+
const skillFiles = findSkillFiles(skillsDir);
|
|
82
|
+
if (skillFiles.length === 0) {
|
|
83
|
+
console.error("No SKILL.md files found");
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
for (const filePath of skillFiles) {
|
|
87
|
+
const rel = relative(process.cwd(), filePath);
|
|
88
|
+
const content = readFileSync(filePath, "utf8");
|
|
89
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)/);
|
|
90
|
+
if (!match) {
|
|
91
|
+
errors.push({
|
|
92
|
+
file: rel,
|
|
93
|
+
message: "Missing or invalid frontmatter"
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (!match[1]) {
|
|
98
|
+
errors.push({
|
|
99
|
+
file: rel,
|
|
100
|
+
message: "Missing YAML frontmatter"
|
|
101
|
+
});
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
let fm;
|
|
105
|
+
try {
|
|
106
|
+
fm = parse(match[1]);
|
|
107
|
+
} catch {
|
|
108
|
+
errors.push({
|
|
109
|
+
file: rel,
|
|
110
|
+
message: "Invalid YAML frontmatter"
|
|
111
|
+
});
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!fm.name) errors.push({
|
|
115
|
+
file: rel,
|
|
116
|
+
message: "Missing required field: name"
|
|
117
|
+
});
|
|
118
|
+
if (!fm.description) errors.push({
|
|
119
|
+
file: rel,
|
|
120
|
+
message: "Missing required field: description"
|
|
121
|
+
});
|
|
122
|
+
if (typeof fm.name === "string") {
|
|
123
|
+
const expectedPath = relative(skillsDir, filePath).replace(/[/\\]SKILL\.md$/, "").split(sep).join("/");
|
|
124
|
+
if (fm.name !== expectedPath) errors.push({
|
|
125
|
+
file: rel,
|
|
126
|
+
message: `name "${fm.name}" does not match directory path "${expectedPath}"`
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
if (fm.type === "framework" && !Array.isArray(fm.requires)) errors.push({
|
|
130
|
+
file: rel,
|
|
131
|
+
message: "Framework skills must have a \"requires\" field"
|
|
132
|
+
});
|
|
133
|
+
const lineCount = content.split(/\r?\n/).length;
|
|
134
|
+
if (lineCount > 500) errors.push({
|
|
135
|
+
file: rel,
|
|
136
|
+
message: `Exceeds 500 line limit (${lineCount} lines)`
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const artifactsDir = join(skillsDir, "_artifacts");
|
|
140
|
+
if (existsSync(artifactsDir)) for (const fileName of [
|
|
141
|
+
"domain_map.yaml",
|
|
142
|
+
"skill_spec.md",
|
|
143
|
+
"skill_tree.yaml"
|
|
144
|
+
]) {
|
|
145
|
+
const artifactPath = join(artifactsDir, fileName);
|
|
146
|
+
if (!existsSync(artifactPath)) {
|
|
147
|
+
errors.push({
|
|
148
|
+
file: relative(process.cwd(), artifactPath),
|
|
149
|
+
message: "Missing required artifact"
|
|
150
|
+
});
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
const content = readFileSync(artifactPath, "utf8");
|
|
154
|
+
if (content.trim().length === 0) {
|
|
155
|
+
errors.push({
|
|
156
|
+
file: relative(process.cwd(), artifactPath),
|
|
157
|
+
message: "Artifact file is empty"
|
|
158
|
+
});
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (fileName.endsWith(".yaml")) try {
|
|
162
|
+
parse(content);
|
|
163
|
+
} catch {
|
|
164
|
+
errors.push({
|
|
165
|
+
file: relative(process.cwd(), artifactPath),
|
|
166
|
+
message: "Invalid YAML in artifact file"
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (errors.length > 0) {
|
|
171
|
+
console.error(`\n❌ Validation failed with ${errors.length} error(s):\n`);
|
|
172
|
+
for (const { file, message } of errors) console.error(` ${file}: ${message}`);
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
console.log(`✅ Validated ${skillFiles.length} skill files — all passed`);
|
|
176
|
+
}
|
|
177
|
+
function cmdScaffold() {
|
|
178
|
+
function tryCopyToClipboard(text) {
|
|
179
|
+
const platform = process.platform;
|
|
180
|
+
const isWsl = platform === "linux" && (Boolean(process.env.WSL_DISTRO_NAME) || Boolean(process.env.WSL_INTEROP) || release().toLowerCase().includes("microsoft"));
|
|
181
|
+
const tryCommand = (command$1, args = []) => {
|
|
182
|
+
return spawnSync(command$1, args, { input: text }).status === 0;
|
|
183
|
+
};
|
|
184
|
+
if (platform === "darwin") return tryCommand("pbcopy");
|
|
185
|
+
if (platform === "win32") return tryCommand("clip");
|
|
186
|
+
if (isWsl) return tryCommand("clip.exe");
|
|
187
|
+
return tryCommand("wl-copy") || tryCommand("xclip", ["-selection", "clipboard"]) || tryCommand("xsel", ["--clipboard", "--input"]);
|
|
188
|
+
}
|
|
189
|
+
const prompt = `You are an AI assistant helping a library maintainer scaffold Intent skills.
|
|
190
|
+
You MUST use the Intent meta skills in this exact order and follow their output requirements.
|
|
191
|
+
|
|
192
|
+
Before you start, ask the maintainer for their skills root path.
|
|
193
|
+
- Default: skills/
|
|
194
|
+
- If they choose a different path, replace "skills/" in all output paths below.
|
|
195
|
+
|
|
196
|
+
1) Meta skill: domain-discovery
|
|
197
|
+
- Input: library name, repo URL, docs URL(s), scope constraints, target audience.
|
|
198
|
+
- Output files (exact paths):
|
|
199
|
+
- skills/_artifacts/domain_map.yaml
|
|
200
|
+
- skills/_artifacts/skill_spec.md
|
|
201
|
+
- These artifacts are maintainer-owned and should be committed to the repo.
|
|
202
|
+
|
|
203
|
+
2) Meta skill: tree-generator
|
|
204
|
+
- Input: skills/_artifacts/domain_map.yaml + skills/_artifacts/skill_spec.md
|
|
205
|
+
- Output file (exact path):
|
|
206
|
+
- skills/_artifacts/skill_tree.yaml
|
|
207
|
+
|
|
208
|
+
3) Meta skill: generate-skill
|
|
209
|
+
- Input: skills/_artifacts/skill_tree.yaml
|
|
210
|
+
- Output files (exact path pattern):
|
|
211
|
+
- skills/<domain>/<skill>/SKILL.md
|
|
212
|
+
|
|
213
|
+
Guidance for the maintainer:
|
|
214
|
+
- If any input is missing, ask for it.
|
|
215
|
+
- After each step, clearly tell the maintainer what files to create and where to save them.
|
|
216
|
+
- Do not skip steps.
|
|
217
|
+
- Use the library's actual terminology from docs and source.
|
|
218
|
+
|
|
219
|
+
At the end, produce a single Markdown feedback doc with three sections (Domain Discovery, Tree Generator, Generate Skill).
|
|
220
|
+
Ask if the maintainer wants to edit it, then ask if you should send it as a GitHub issue to TanStack/intent.
|
|
221
|
+
Use the issue title: [meta-feedback] intent meta skill.
|
|
222
|
+
|
|
223
|
+
Finish with a short checklist:
|
|
224
|
+
- Run npx intent validate
|
|
225
|
+
- Commit skills/ and skills/_artifacts/ (artifacts are repo-only)
|
|
226
|
+
- Exclude skills/_artifacts/ from package publishing
|
|
227
|
+
- Add README snippet: If you use an AI agent, run npx intent init
|
|
228
|
+
`;
|
|
229
|
+
console.log("🚀 Intent Scaffold Prompt");
|
|
230
|
+
console.log("✨ Copy the prompt below into your AI agent:\n");
|
|
231
|
+
console.log(prompt);
|
|
232
|
+
if (tryCopyToClipboard(prompt)) console.log("\n✅ Copied prompt to clipboard");
|
|
233
|
+
else console.log("\n⚠ Tip: Manually copy the prompt above into your agent");
|
|
234
|
+
}
|
|
235
|
+
const USAGE = `TanStack Intent CLI
|
|
236
|
+
|
|
237
|
+
Usage:
|
|
238
|
+
intent list [--json] Discover intent-enabled packages
|
|
239
|
+
intent meta List meta-skills for maintainers
|
|
240
|
+
intent validate [<dir>] Validate skill files (default: skills/)
|
|
241
|
+
intent init Set up intent discovery in agent configs
|
|
242
|
+
intent scaffold Print maintainer scaffold prompt
|
|
243
|
+
intent setup [--workflows] [--oz] [--all] Copy CI/Oz templates into your repo
|
|
244
|
+
intent stale Check skills for staleness
|
|
245
|
+
intent feedback --submit --file <path> Submit skill feedback
|
|
246
|
+
intent feedback --meta --submit --file <path> Submit meta-skill feedback`;
|
|
247
|
+
const command = process.argv[2];
|
|
248
|
+
const commandArgs = process.argv.slice(3);
|
|
249
|
+
switch (command) {
|
|
250
|
+
case "list":
|
|
251
|
+
await cmdList(commandArgs);
|
|
252
|
+
break;
|
|
253
|
+
case "meta":
|
|
254
|
+
cmdMeta();
|
|
255
|
+
break;
|
|
256
|
+
case "validate":
|
|
257
|
+
cmdValidate(commandArgs);
|
|
258
|
+
break;
|
|
259
|
+
case "init": {
|
|
260
|
+
const { runInit, detectAgentConfigs } = await import("./init-DEzzXm9j.mjs");
|
|
261
|
+
const initRoot = process.cwd();
|
|
262
|
+
const result = runInit(initRoot);
|
|
263
|
+
for (const f of result.injected) console.log(`✓ Added intent block to ${f}`);
|
|
264
|
+
for (const f of result.skipped) console.log(` Already present in ${f}`);
|
|
265
|
+
for (const f of result.created) console.log(`✓ Created ${f}`);
|
|
266
|
+
if (result.injected.length === 0 && result.skipped.length === 0) {
|
|
267
|
+
if (detectAgentConfigs(initRoot).length === 0) {
|
|
268
|
+
console.log("No agent config files found (AGENTS.md, CLAUDE.md, .cursorrules, .github/copilot-instructions.md).");
|
|
269
|
+
console.log("Create one of these files and run intent init again.");
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
console.log(`✓ Config: ${result.configPath}`);
|
|
273
|
+
break;
|
|
274
|
+
}
|
|
275
|
+
case "scaffold":
|
|
276
|
+
cmdScaffold();
|
|
277
|
+
break;
|
|
278
|
+
case "stale": {
|
|
279
|
+
const { checkStaleness } = await import("./staleness-DyhsrqQ5.mjs");
|
|
280
|
+
const { scanForIntents: scanStale } = await import("./scanner-BuWPDJ4P.mjs");
|
|
281
|
+
let staleResult;
|
|
282
|
+
try {
|
|
283
|
+
staleResult = await scanStale();
|
|
284
|
+
} catch (err) {
|
|
285
|
+
console.error(err.message);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
if (staleResult.packages.length === 0) {
|
|
289
|
+
console.log("No intent-enabled packages found.");
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
const jsonStale = commandArgs.includes("--json");
|
|
293
|
+
const reports = await Promise.all(staleResult.packages.map((pkg) => {
|
|
294
|
+
return checkStaleness(join(process.cwd(), "node_modules", pkg.name), pkg.name);
|
|
295
|
+
}));
|
|
296
|
+
if (jsonStale) {
|
|
297
|
+
console.log(JSON.stringify(reports, null, 2));
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
for (const report of reports) {
|
|
301
|
+
const driftLabel = report.versionDrift ? ` [${report.versionDrift} drift]` : "";
|
|
302
|
+
const vLabel = report.skillVersion && report.currentVersion ? ` (${report.skillVersion} → ${report.currentVersion})` : "";
|
|
303
|
+
console.log(`${report.library}${vLabel}${driftLabel}`);
|
|
304
|
+
const stale = report.skills.filter((s) => s.needsReview);
|
|
305
|
+
if (stale.length === 0) console.log(" All skills up-to-date");
|
|
306
|
+
else for (const skill of stale) console.log(` ⚠ ${skill.name}: ${skill.reasons.join(", ")}`);
|
|
307
|
+
console.log();
|
|
308
|
+
}
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
case "feedback": {
|
|
312
|
+
const { runFeedback } = await import("./feedback-FIUBOL0g.mjs");
|
|
313
|
+
runFeedback(commandArgs);
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
316
|
+
case "setup": {
|
|
317
|
+
const { runSetup } = await import("./setup.mjs");
|
|
318
|
+
runSetup(process.cwd(), getMetaDir(), commandArgs);
|
|
319
|
+
break;
|
|
320
|
+
}
|
|
321
|
+
default:
|
|
322
|
+
console.log(USAGE);
|
|
323
|
+
process.exit(command ? 1 : 0);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
//#endregion
|
|
327
|
+
export { };
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
//#region src/feedback.ts
|
|
6
|
+
const META_FEEDBACK_REPO = "TanStack/intent";
|
|
7
|
+
const SECRET_PATTERNS = [
|
|
8
|
+
/(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,}/,
|
|
9
|
+
/(?:sk|pk)[-_](?:live|test)[-_][A-Za-z0-9]{24,}/,
|
|
10
|
+
/AKIA[0-9A-Z]{16}/,
|
|
11
|
+
/-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/,
|
|
12
|
+
/eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
|
|
13
|
+
/(?:Bearer|token)\s+[A-Za-z0-9_\-.~+/]{20,}/i,
|
|
14
|
+
/[A-Za-z0-9]{32,}(?=.*(?:key|secret|token|password))/i
|
|
15
|
+
];
|
|
16
|
+
function containsSecrets(text) {
|
|
17
|
+
return SECRET_PATTERNS.some((pattern) => pattern.test(text));
|
|
18
|
+
}
|
|
19
|
+
function hasGhCli() {
|
|
20
|
+
try {
|
|
21
|
+
execSync("gh --version", { stdio: "ignore" });
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function getHomeConfigDir() {
|
|
28
|
+
return process.env.XDG_CONFIG_HOME ?? join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".config");
|
|
29
|
+
}
|
|
30
|
+
function resolveFrequency(root) {
|
|
31
|
+
const userConfigPath = join(getHomeConfigDir(), "intent", "config.json");
|
|
32
|
+
try {
|
|
33
|
+
const userCfg = JSON.parse(readFileSync(userConfigPath, "utf8"));
|
|
34
|
+
if (userCfg.feedback?.frequency) return userCfg.feedback.frequency;
|
|
35
|
+
} catch {}
|
|
36
|
+
const projectConfigPath = join(root, "intent.config.json");
|
|
37
|
+
try {
|
|
38
|
+
const projCfg = JSON.parse(readFileSync(projectConfigPath, "utf8"));
|
|
39
|
+
if (projCfg.feedback?.frequency) return projCfg.feedback.frequency;
|
|
40
|
+
} catch {}
|
|
41
|
+
return "every-5";
|
|
42
|
+
}
|
|
43
|
+
const REQUIRED_FIELDS = [
|
|
44
|
+
"skill",
|
|
45
|
+
"package",
|
|
46
|
+
"skillVersion",
|
|
47
|
+
"task",
|
|
48
|
+
"whatWorked",
|
|
49
|
+
"whatFailed",
|
|
50
|
+
"missing",
|
|
51
|
+
"selfCorrections",
|
|
52
|
+
"userRating"
|
|
53
|
+
];
|
|
54
|
+
function validatePayload(payload) {
|
|
55
|
+
const errors = [];
|
|
56
|
+
if (!payload || typeof payload !== "object") return {
|
|
57
|
+
valid: false,
|
|
58
|
+
errors: ["Payload must be a JSON object"]
|
|
59
|
+
};
|
|
60
|
+
const obj = payload;
|
|
61
|
+
for (const field of REQUIRED_FIELDS) if (typeof obj[field] !== "string" || obj[field].trim() === "") errors.push(`Missing or empty required field: ${field}`);
|
|
62
|
+
if (obj.userRating && ![
|
|
63
|
+
"good",
|
|
64
|
+
"mixed",
|
|
65
|
+
"bad"
|
|
66
|
+
].includes(obj.userRating)) errors.push("userRating must be one of: good, mixed, bad");
|
|
67
|
+
if (containsSecrets(Object.values(obj).filter((v) => typeof v === "string").join("\n"))) errors.push("Payload appears to contain secrets or tokens — submission rejected");
|
|
68
|
+
return {
|
|
69
|
+
valid: errors.length === 0,
|
|
70
|
+
errors
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const META_REQUIRED_FIELDS = [
|
|
74
|
+
"metaSkill",
|
|
75
|
+
"library",
|
|
76
|
+
"agentUsed",
|
|
77
|
+
"artifactQuality",
|
|
78
|
+
"whatWorked",
|
|
79
|
+
"whatFailed",
|
|
80
|
+
"suggestions",
|
|
81
|
+
"userRating"
|
|
82
|
+
];
|
|
83
|
+
const VALID_META_SKILLS = [
|
|
84
|
+
"domain-discovery",
|
|
85
|
+
"tree-generator",
|
|
86
|
+
"generate-skill",
|
|
87
|
+
"skill-staleness-check"
|
|
88
|
+
];
|
|
89
|
+
const VALID_AGENTS = [
|
|
90
|
+
"oz",
|
|
91
|
+
"claude-code",
|
|
92
|
+
"cursor",
|
|
93
|
+
"copilot",
|
|
94
|
+
"codex",
|
|
95
|
+
"other"
|
|
96
|
+
];
|
|
97
|
+
const VALID_QUALITY_RATINGS = [
|
|
98
|
+
"good",
|
|
99
|
+
"mixed",
|
|
100
|
+
"bad"
|
|
101
|
+
];
|
|
102
|
+
function validateMetaPayload(payload) {
|
|
103
|
+
const errors = [];
|
|
104
|
+
if (!payload || typeof payload !== "object") return {
|
|
105
|
+
valid: false,
|
|
106
|
+
errors: ["Payload must be a JSON object"]
|
|
107
|
+
};
|
|
108
|
+
const obj = payload;
|
|
109
|
+
for (const field of META_REQUIRED_FIELDS) if (typeof obj[field] !== "string" || obj[field].trim() === "") errors.push(`Missing or empty required field: ${field}`);
|
|
110
|
+
if (obj.metaSkill && !VALID_META_SKILLS.includes(obj.metaSkill)) errors.push(`metaSkill must be one of: ${VALID_META_SKILLS.join(", ")}`);
|
|
111
|
+
if (obj.agentUsed && !VALID_AGENTS.includes(obj.agentUsed)) errors.push(`agentUsed must be one of: ${VALID_AGENTS.join(", ")}`);
|
|
112
|
+
if (obj.artifactQuality && !VALID_QUALITY_RATINGS.includes(obj.artifactQuality)) errors.push("artifactQuality must be one of: good, mixed, bad");
|
|
113
|
+
if (obj.userRating && !VALID_QUALITY_RATINGS.includes(obj.userRating)) errors.push("userRating must be one of: good, mixed, bad");
|
|
114
|
+
if (containsSecrets(Object.values(obj).filter((v) => typeof v === "string").join("\n"))) errors.push("Payload appears to contain secrets or tokens — submission rejected");
|
|
115
|
+
return {
|
|
116
|
+
valid: errors.length === 0,
|
|
117
|
+
errors
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function metaToMarkdown(payload) {
|
|
121
|
+
const lines = [
|
|
122
|
+
`# Meta-Skill Feedback: ${payload.metaSkill}`,
|
|
123
|
+
"",
|
|
124
|
+
`**Library:** ${payload.library}`,
|
|
125
|
+
`**Agent:** ${payload.agentUsed}`,
|
|
126
|
+
`**Artifact quality:** ${payload.artifactQuality}`,
|
|
127
|
+
`**Rating:** ${payload.userRating}`
|
|
128
|
+
];
|
|
129
|
+
if (payload.interviewQuality) lines.push(`**Interview quality:** ${payload.interviewQuality}`);
|
|
130
|
+
if (payload.failureModeQuality) lines.push(`**Failure mode quality:** ${payload.failureModeQuality}`);
|
|
131
|
+
lines.push("", "## What Worked", payload.whatWorked, "", "## What Failed", payload.whatFailed, "", "## Suggestions", payload.suggestions);
|
|
132
|
+
return lines.join("\n") + "\n";
|
|
133
|
+
}
|
|
134
|
+
function toMarkdown(payload) {
|
|
135
|
+
const lines = [
|
|
136
|
+
`# Skill Feedback: ${payload.skill}`,
|
|
137
|
+
"",
|
|
138
|
+
`**Package:** ${payload.package}`,
|
|
139
|
+
`**Skill version:** ${payload.skillVersion}`,
|
|
140
|
+
`**Rating:** ${payload.userRating}`,
|
|
141
|
+
"",
|
|
142
|
+
"## Task",
|
|
143
|
+
payload.task,
|
|
144
|
+
"",
|
|
145
|
+
"## What Worked",
|
|
146
|
+
payload.whatWorked,
|
|
147
|
+
"",
|
|
148
|
+
"## What Failed",
|
|
149
|
+
payload.whatFailed,
|
|
150
|
+
"",
|
|
151
|
+
"## Missing",
|
|
152
|
+
payload.missing,
|
|
153
|
+
"",
|
|
154
|
+
"## Self-Corrections",
|
|
155
|
+
payload.selfCorrections
|
|
156
|
+
];
|
|
157
|
+
if (payload.userComments) lines.push("", "## User Comments", payload.userComments);
|
|
158
|
+
return lines.join("\n") + "\n";
|
|
159
|
+
}
|
|
160
|
+
function submitFeedback(payload, repo, opts) {
|
|
161
|
+
const md = toMarkdown(payload);
|
|
162
|
+
if (opts.ghAvailable) try {
|
|
163
|
+
execSync(`gh issue create --repo ${repo} --title "${`Skill Feedback: ${payload.skill} (${payload.userRating})`.replace(/"/g, "\\\"")}" --body -`, {
|
|
164
|
+
input: md,
|
|
165
|
+
stdio: [
|
|
166
|
+
"pipe",
|
|
167
|
+
"pipe",
|
|
168
|
+
"pipe"
|
|
169
|
+
]
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
method: "gh",
|
|
173
|
+
detail: `Submitted issue to ${repo}`
|
|
174
|
+
};
|
|
175
|
+
} catch {}
|
|
176
|
+
if (opts.outputPath) {
|
|
177
|
+
writeFileSync(opts.outputPath, md, "utf8");
|
|
178
|
+
return {
|
|
179
|
+
method: "file",
|
|
180
|
+
detail: `Saved to ${opts.outputPath}`
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
method: "stdout",
|
|
185
|
+
detail: md
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function submitMetaFeedback(payload, opts) {
|
|
189
|
+
const md = metaToMarkdown(payload);
|
|
190
|
+
if (opts.ghAvailable) try {
|
|
191
|
+
execSync(`gh issue create --repo ${META_FEEDBACK_REPO} --title "${`Meta-Skill Feedback: ${payload.metaSkill} (${payload.userRating})`.replace(/"/g, "\\\"")}" --label "feedback:${payload.metaSkill}" --body -`, {
|
|
192
|
+
input: md,
|
|
193
|
+
stdio: [
|
|
194
|
+
"pipe",
|
|
195
|
+
"pipe",
|
|
196
|
+
"pipe"
|
|
197
|
+
]
|
|
198
|
+
});
|
|
199
|
+
return {
|
|
200
|
+
method: "gh",
|
|
201
|
+
detail: `Submitted issue to ${META_FEEDBACK_REPO}`
|
|
202
|
+
};
|
|
203
|
+
} catch {}
|
|
204
|
+
if (opts.outputPath) {
|
|
205
|
+
writeFileSync(opts.outputPath, md, "utf8");
|
|
206
|
+
return {
|
|
207
|
+
method: "file",
|
|
208
|
+
detail: `Saved to ${opts.outputPath}`
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
method: "stdout",
|
|
213
|
+
detail: md
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function runFeedback(args) {
|
|
217
|
+
const isMeta = args.includes("--meta");
|
|
218
|
+
const submitFlag = args.includes("--submit");
|
|
219
|
+
const fileIdx = args.indexOf("--file");
|
|
220
|
+
const filePath = fileIdx !== -1 ? args[fileIdx + 1] : void 0;
|
|
221
|
+
if (!submitFlag || !filePath) {
|
|
222
|
+
if (isMeta) console.error("Usage: intent feedback --meta --submit --file <path>");
|
|
223
|
+
else console.error("Usage: intent feedback --submit --file <path>");
|
|
224
|
+
process.exit(1);
|
|
225
|
+
}
|
|
226
|
+
if (!existsSync(filePath)) {
|
|
227
|
+
console.error(`File not found: ${filePath}`);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
let raw;
|
|
231
|
+
try {
|
|
232
|
+
raw = JSON.parse(readFileSync(filePath, "utf8"));
|
|
233
|
+
} catch {
|
|
234
|
+
console.error("Invalid JSON in feedback file");
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
const ghAvailable = hasGhCli();
|
|
238
|
+
if (resolveFrequency(process.cwd()) === "never") {
|
|
239
|
+
console.log("Feedback is disabled (frequency: never)");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const dateSuffix = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
243
|
+
if (isMeta) {
|
|
244
|
+
const validation$1 = validateMetaPayload(raw);
|
|
245
|
+
if (!validation$1.valid) {
|
|
246
|
+
console.error("Meta-feedback validation failed:");
|
|
247
|
+
for (const err of validation$1.errors) console.error(` - ${err}`);
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
const payload$1 = raw;
|
|
251
|
+
const fallbackPath$1 = `intent-meta-feedback-${dateSuffix}.md`;
|
|
252
|
+
const result$1 = submitMetaFeedback(payload$1, {
|
|
253
|
+
ghAvailable,
|
|
254
|
+
outputPath: ghAvailable ? void 0 : fallbackPath$1
|
|
255
|
+
});
|
|
256
|
+
switch (result$1.method) {
|
|
257
|
+
case "gh":
|
|
258
|
+
console.log(`✓ ${result$1.detail}`);
|
|
259
|
+
break;
|
|
260
|
+
case "file":
|
|
261
|
+
console.log(`✓ ${result$1.detail}`);
|
|
262
|
+
console.log(`Open a GitHub Discussion at https://github.com/${META_FEEDBACK_REPO}/discussions/new?category=Feedback`);
|
|
263
|
+
break;
|
|
264
|
+
case "stdout":
|
|
265
|
+
console.log("--- Meta-feedback markdown (copy/paste to discussion) ---");
|
|
266
|
+
console.log(result$1.detail);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
const validation = validatePayload(raw);
|
|
272
|
+
if (!validation.valid) {
|
|
273
|
+
console.error("Feedback validation failed:");
|
|
274
|
+
for (const err of validation.errors) console.error(` - ${err}`);
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
const payload = raw;
|
|
278
|
+
const repo = payload.package.replace(/^@/, "").replace(/\//, "/");
|
|
279
|
+
const fallbackPath = `intent-feedback-${dateSuffix}.md`;
|
|
280
|
+
const result = submitFeedback(payload, repo, {
|
|
281
|
+
ghAvailable,
|
|
282
|
+
outputPath: ghAvailable ? void 0 : fallbackPath
|
|
283
|
+
});
|
|
284
|
+
switch (result.method) {
|
|
285
|
+
case "gh":
|
|
286
|
+
console.log(`✓ ${result.detail}`);
|
|
287
|
+
break;
|
|
288
|
+
case "file":
|
|
289
|
+
console.log(`✓ ${result.detail}`);
|
|
290
|
+
console.log("You can manually open an issue with this content.");
|
|
291
|
+
break;
|
|
292
|
+
case "stdout":
|
|
293
|
+
console.log("--- Feedback markdown (copy/paste to issue) ---");
|
|
294
|
+
console.log(result.detail);
|
|
295
|
+
break;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
//#endregion
|
|
300
|
+
export { runFeedback as a, toMarkdown as c, resolveFrequency as i, validateMetaPayload as l, hasGhCli as n, submitFeedback as o, metaToMarkdown as r, submitMetaFeedback as s, containsSecrets as t, validatePayload as u };
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { a as runFeedback, c as toMarkdown, i as resolveFrequency, l as validateMetaPayload, n as hasGhCli, o as submitFeedback, r as metaToMarkdown, s as submitMetaFeedback, t as containsSecrets, u as validatePayload } from "./feedback-DKreHfB1.mjs";
|
|
2
|
+
|
|
3
|
+
export { runFeedback };
|