skrypt-ai 0.6.0 → 0.7.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.
Files changed (88) hide show
  1. package/dist/audit/doc-parser.d.ts +5 -0
  2. package/dist/audit/doc-parser.js +106 -0
  3. package/dist/audit/index.d.ts +4 -0
  4. package/dist/audit/index.js +4 -0
  5. package/dist/audit/matcher.d.ts +6 -0
  6. package/dist/audit/matcher.js +94 -0
  7. package/dist/audit/reporter.d.ts +9 -0
  8. package/dist/audit/reporter.js +106 -0
  9. package/dist/audit/types.d.ts +37 -0
  10. package/dist/audit/types.js +1 -0
  11. package/dist/auth/index.js +3 -1
  12. package/dist/cli.js +11 -1
  13. package/dist/commands/audit.d.ts +2 -0
  14. package/dist/commands/audit.js +59 -0
  15. package/dist/commands/config.d.ts +2 -0
  16. package/dist/commands/config.js +73 -0
  17. package/dist/commands/cron.js +4 -0
  18. package/dist/commands/generate.d.ts +7 -0
  19. package/dist/commands/generate.js +528 -234
  20. package/dist/commands/refresh.d.ts +2 -0
  21. package/dist/commands/refresh.js +158 -0
  22. package/dist/commands/review-pr.js +5 -0
  23. package/dist/commands/review.d.ts +2 -0
  24. package/dist/commands/review.js +110 -0
  25. package/dist/commands/test.js +177 -236
  26. package/dist/commands/watch.js +29 -20
  27. package/dist/config/loader.d.ts +6 -1
  28. package/dist/config/loader.js +38 -2
  29. package/dist/config/types.d.ts +7 -0
  30. package/dist/generator/generator.js +2 -1
  31. package/dist/generator/types.d.ts +3 -0
  32. package/dist/generator/writer.js +60 -28
  33. package/dist/github/org-discovery.d.ts +17 -0
  34. package/dist/github/org-discovery.js +93 -0
  35. package/dist/llm/index.d.ts +2 -0
  36. package/dist/llm/index.js +8 -2
  37. package/dist/next-actions/actions.d.ts +2 -0
  38. package/dist/next-actions/actions.js +190 -0
  39. package/dist/next-actions/index.d.ts +6 -0
  40. package/dist/next-actions/index.js +39 -0
  41. package/dist/next-actions/setup.d.ts +2 -0
  42. package/dist/next-actions/setup.js +72 -0
  43. package/dist/next-actions/state.d.ts +7 -0
  44. package/dist/next-actions/state.js +68 -0
  45. package/dist/next-actions/suggest.d.ts +3 -0
  46. package/dist/next-actions/suggest.js +47 -0
  47. package/dist/next-actions/types.d.ts +26 -0
  48. package/dist/next-actions/types.js +1 -0
  49. package/dist/refresh/differ.d.ts +9 -0
  50. package/dist/refresh/differ.js +67 -0
  51. package/dist/refresh/index.d.ts +4 -0
  52. package/dist/refresh/index.js +4 -0
  53. package/dist/refresh/manifest.d.ts +18 -0
  54. package/dist/refresh/manifest.js +71 -0
  55. package/dist/refresh/splicer.d.ts +9 -0
  56. package/dist/refresh/splicer.js +50 -0
  57. package/dist/refresh/types.d.ts +37 -0
  58. package/dist/refresh/types.js +1 -0
  59. package/dist/review/index.d.ts +8 -0
  60. package/dist/review/index.js +94 -0
  61. package/dist/review/parser.d.ts +16 -0
  62. package/dist/review/parser.js +95 -0
  63. package/dist/review/types.d.ts +18 -0
  64. package/dist/review/types.js +1 -0
  65. package/dist/scanner/types.d.ts +2 -0
  66. package/dist/structure/index.d.ts +19 -0
  67. package/dist/structure/index.js +92 -0
  68. package/dist/structure/planner.d.ts +8 -0
  69. package/dist/structure/planner.js +180 -0
  70. package/dist/structure/topology.d.ts +16 -0
  71. package/dist/structure/topology.js +49 -0
  72. package/dist/structure/types.d.ts +26 -0
  73. package/dist/structure/types.js +1 -0
  74. package/dist/testing/comparator.d.ts +7 -0
  75. package/dist/testing/comparator.js +77 -0
  76. package/dist/testing/docker.d.ts +21 -0
  77. package/dist/testing/docker.js +234 -0
  78. package/dist/testing/env.d.ts +16 -0
  79. package/dist/testing/env.js +58 -0
  80. package/dist/testing/extractor.d.ts +9 -0
  81. package/dist/testing/extractor.js +195 -0
  82. package/dist/testing/index.d.ts +6 -0
  83. package/dist/testing/index.js +6 -0
  84. package/dist/testing/runner.d.ts +5 -0
  85. package/dist/testing/runner.js +225 -0
  86. package/dist/testing/types.d.ts +58 -0
  87. package/dist/testing/types.js +1 -0
  88. package/package.json +1 -1
@@ -0,0 +1,72 @@
1
+ import * as readline from 'readline';
2
+ import { writePreferences, DEFAULT_PREFERENCES } from './state.js';
3
+ function ask(rl, question) {
4
+ return new Promise((resolve, reject) => {
5
+ rl.question(question, answer => resolve(answer.trim()));
6
+ rl.once('close', () => reject(new Error('EOF')));
7
+ });
8
+ }
9
+ export async function runFirstTimeSetup() {
10
+ console.log('');
11
+ console.log(' \x1b[36m─────────────────────────────────────\x1b[0m');
12
+ console.log('');
13
+ console.log(' Skrypt can suggest next steps after each command.');
14
+ console.log('');
15
+ console.log(' 1) All suggestions (recommended)');
16
+ console.log(' 2) Workflow only (generate \u2192 test \u2192 deploy)');
17
+ console.log(' 3) Customize');
18
+ console.log(' 4) Disable');
19
+ console.log('');
20
+ const rl = readline.createInterface({
21
+ input: process.stdin,
22
+ output: process.stdout,
23
+ });
24
+ try {
25
+ const choice = await ask(rl, ' Choice [1]: ');
26
+ const selected = choice || '1';
27
+ let prefs;
28
+ if (selected === '4') {
29
+ prefs = {
30
+ enabled: false,
31
+ categories: { workflow: false, quality: false, cicd: false, advanced: false },
32
+ };
33
+ }
34
+ else if (selected === '2') {
35
+ prefs = {
36
+ enabled: true,
37
+ categories: { workflow: true, quality: false, cicd: false, advanced: false },
38
+ };
39
+ }
40
+ else if (selected === '3') {
41
+ console.log('');
42
+ const w = await ask(rl, ' Workflow (generate, test, deploy)? [Y/n] ');
43
+ const q = await ask(rl, ' Quality (qa, lint, security)? [Y/n] ');
44
+ const c = await ask(rl, ' CI/CD (GitHub Actions, deploy)? [Y/n] ');
45
+ const a = await ask(rl, ' Advanced (audit, refresh, review)? [Y/n] ');
46
+ prefs = {
47
+ enabled: true,
48
+ categories: {
49
+ workflow: w.toLowerCase() !== 'n',
50
+ quality: q.toLowerCase() !== 'n',
51
+ cicd: c.toLowerCase() !== 'n',
52
+ advanced: a.toLowerCase() !== 'n',
53
+ },
54
+ };
55
+ }
56
+ else {
57
+ prefs = { ...DEFAULT_PREFERENCES };
58
+ }
59
+ writePreferences(prefs);
60
+ console.log('');
61
+ if (prefs.enabled) {
62
+ console.log(' \x1b[32mSaved!\x1b[0m Change anytime with: skrypt config --suggestions');
63
+ }
64
+ else {
65
+ console.log(' Suggestions disabled. Re-enable with: skrypt config --suggestions');
66
+ }
67
+ return prefs;
68
+ }
69
+ finally {
70
+ rl.close();
71
+ }
72
+ }
@@ -0,0 +1,7 @@
1
+ import { NextActionPreferences, ProjectActionState } from './types.js';
2
+ export declare const DEFAULT_PREFERENCES: NextActionPreferences;
3
+ export declare function readPreferences(): NextActionPreferences | null;
4
+ export declare function writePreferences(prefs: NextActionPreferences): void;
5
+ export declare function readProjectState(): ProjectActionState;
6
+ export declare function writeProjectState(state: ProjectActionState): void;
7
+ export declare function markCommandCompleted(commandName: string): void;
@@ -0,0 +1,68 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { homedir } from 'os';
4
+ import { createHash } from 'crypto';
5
+ function skryptHome() {
6
+ return join(homedir(), '.skrypt');
7
+ }
8
+ function preferencesFile() {
9
+ return join(skryptHome(), 'preferences.json');
10
+ }
11
+ function projectsDir() {
12
+ return join(skryptHome(), 'projects');
13
+ }
14
+ export const DEFAULT_PREFERENCES = {
15
+ enabled: true,
16
+ categories: {
17
+ workflow: true,
18
+ quality: true,
19
+ cicd: true,
20
+ advanced: true,
21
+ },
22
+ };
23
+ function projectHash() {
24
+ return createHash('sha256').update(process.cwd()).digest('hex').slice(0, 12);
25
+ }
26
+ function projectStatePath() {
27
+ return join(projectsDir(), `${projectHash()}.json`);
28
+ }
29
+ export function readPreferences() {
30
+ try {
31
+ const path = preferencesFile();
32
+ if (!existsSync(path))
33
+ return null;
34
+ return JSON.parse(readFileSync(path, 'utf-8'));
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ export function writePreferences(prefs) {
41
+ const home = skryptHome();
42
+ mkdirSync(home, { recursive: true });
43
+ writeFileSync(preferencesFile(), JSON.stringify(prefs, null, 2), { mode: 0o600 });
44
+ }
45
+ export function readProjectState() {
46
+ try {
47
+ const path = projectStatePath();
48
+ if (!existsSync(path)) {
49
+ return { completedCommands: [], lastUpdated: '' };
50
+ }
51
+ return JSON.parse(readFileSync(path, 'utf-8'));
52
+ }
53
+ catch {
54
+ return { completedCommands: [], lastUpdated: '' };
55
+ }
56
+ }
57
+ export function writeProjectState(state) {
58
+ mkdirSync(projectsDir(), { recursive: true });
59
+ state.lastUpdated = new Date().toISOString();
60
+ writeFileSync(projectStatePath(), JSON.stringify(state, null, 2), { mode: 0o600 });
61
+ }
62
+ export function markCommandCompleted(commandName) {
63
+ const state = readProjectState();
64
+ if (!state.completedCommands.includes(commandName)) {
65
+ state.completedCommands.push(commandName);
66
+ }
67
+ writeProjectState(state);
68
+ }
@@ -0,0 +1,3 @@
1
+ import { NextActionPreferences, ProjectActionState, Suggestion } from './types.js';
2
+ export declare function getSuggestions(commandName: string, preferences: NextActionPreferences, _state?: ProjectActionState): Suggestion[];
3
+ export declare function printSuggestions(suggestions: Suggestion[]): void;
@@ -0,0 +1,47 @@
1
+ import { ACTION_DEFINITIONS } from './actions.js';
2
+ const MAX_SUGGESTIONS = 2;
3
+ export function getSuggestions(commandName, preferences, _state) {
4
+ if (!preferences.enabled)
5
+ return [];
6
+ const candidates = ACTION_DEFINITIONS
7
+ .filter(action => {
8
+ if (!action.afterCommands.includes(commandName))
9
+ return false;
10
+ if (!preferences.categories[action.category])
11
+ return false;
12
+ if (action.condition && !action.condition())
13
+ return false;
14
+ // Don't suggest the same command that just ran
15
+ if (action.command.startsWith('skrypt ')) {
16
+ const suggestedCmd = action.command.split(' ')[1];
17
+ if (suggestedCmd === commandName)
18
+ return false;
19
+ }
20
+ return true;
21
+ })
22
+ .sort((a, b) => b.priority - a.priority)
23
+ .slice(0, MAX_SUGGESTIONS);
24
+ // Deduplicate by command string
25
+ const seen = new Set();
26
+ const unique = [];
27
+ for (const c of candidates) {
28
+ if (!seen.has(c.command)) {
29
+ seen.add(c.command);
30
+ unique.push({ command: c.command, message: c.message });
31
+ }
32
+ }
33
+ return unique;
34
+ }
35
+ export function printSuggestions(suggestions) {
36
+ if (suggestions.length === 0)
37
+ return;
38
+ console.log('');
39
+ console.log(' \x1b[36m───\x1b[0m');
40
+ console.log('');
41
+ for (const s of suggestions) {
42
+ console.log(` \x1b[36m\u2192\x1b[0m \x1b[1m${s.command}\x1b[0m`);
43
+ console.log(` ${s.message}`);
44
+ }
45
+ console.log('');
46
+ console.log(' \x1b[90mManage: skrypt config --suggestions\x1b[0m');
47
+ }
@@ -0,0 +1,26 @@
1
+ export interface NextActionPreferences {
2
+ enabled: boolean;
3
+ categories: {
4
+ workflow: boolean;
5
+ quality: boolean;
6
+ cicd: boolean;
7
+ advanced: boolean;
8
+ };
9
+ }
10
+ export interface ProjectActionState {
11
+ completedCommands: string[];
12
+ lastUpdated: string;
13
+ }
14
+ export interface ActionDefinition {
15
+ id: string;
16
+ afterCommands: string[];
17
+ category: keyof NextActionPreferences['categories'];
18
+ message: string;
19
+ command: string;
20
+ priority: number;
21
+ condition?: () => boolean;
22
+ }
23
+ export interface Suggestion {
24
+ command: string;
25
+ message: string;
26
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { DocManifest, StaleElement } from './types.js';
2
+ /**
3
+ * Get list of changed files since a given ref using git diff
4
+ */
5
+ export declare function getChangedFiles(since: string, cwd?: string): string[];
6
+ /**
7
+ * Find elements whose signatures have changed since the manifest was written
8
+ */
9
+ export declare function findStaleElements(manifest: DocManifest, changedFiles: string[]): Promise<StaleElement[]>;
@@ -0,0 +1,67 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { scanDirectory } from '../scanner/index.js';
3
+ import { hashSignature } from './manifest.js';
4
+ /**
5
+ * Get list of changed files since a given ref using git diff
6
+ */
7
+ export function getChangedFiles(since, cwd) {
8
+ const result = spawnSync('git', ['diff', '--name-only', `${since}..HEAD`], {
9
+ stdio: 'pipe',
10
+ cwd,
11
+ timeout: 10000,
12
+ });
13
+ if (result.status !== 0) {
14
+ const stderr = result.stderr?.toString() || '';
15
+ throw new Error(`git diff failed: ${stderr}`);
16
+ }
17
+ return result.stdout
18
+ .toString()
19
+ .trim()
20
+ .split('\n')
21
+ .filter(Boolean);
22
+ }
23
+ /**
24
+ * Find elements whose signatures have changed since the manifest was written
25
+ */
26
+ export async function findStaleElements(manifest, changedFiles) {
27
+ const stale = [];
28
+ // Find manifest entries whose source files have changed
29
+ const affectedEntries = manifest.elements.filter(entry => changedFiles.some(f => entry.sourceFile === f ||
30
+ entry.sourceFile.endsWith('/' + f) ||
31
+ f.endsWith('/' + entry.sourceFile)));
32
+ if (affectedEntries.length === 0) {
33
+ return stale;
34
+ }
35
+ // Re-scan the changed source files to get current signatures
36
+ const filesToScan = [...new Set(affectedEntries.map(e => e.sourceFile))];
37
+ for (const file of filesToScan) {
38
+ try {
39
+ const scanResult = await scanDirectory(file);
40
+ const currentElements = scanResult.files.flatMap(f => f.elements);
41
+ for (const entry of affectedEntries.filter(e => e.sourceFile === file)) {
42
+ const current = currentElements.find(el => el.name === entry.name && el.kind === entry.kind);
43
+ if (!current) {
44
+ // Element was removed from code
45
+ stale.push({
46
+ entry,
47
+ newSignature: '[REMOVED]',
48
+ newSignatureHash: '',
49
+ });
50
+ continue;
51
+ }
52
+ const currentHash = hashSignature(current.signature);
53
+ if (currentHash !== entry.signatureHash) {
54
+ stale.push({
55
+ entry,
56
+ newSignature: current.signature,
57
+ newSignatureHash: currentHash,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ catch (err) {
63
+ console.warn(` Warning: Could not scan ${file}: ${err instanceof Error ? err.message : err}`);
64
+ }
65
+ }
66
+ return stale;
67
+ }
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './manifest.js';
3
+ export * from './differ.js';
4
+ export * from './splicer.js';
@@ -0,0 +1,4 @@
1
+ export * from './types.js';
2
+ export * from './manifest.js';
3
+ export * from './differ.js';
4
+ export * from './splicer.js';
@@ -0,0 +1,18 @@
1
+ import { APIElement } from '../scanner/types.js';
2
+ import { DocManifest, ManifestEntry } from './types.js';
3
+ /**
4
+ * Hash a signature string for change detection
5
+ */
6
+ export declare function hashSignature(signature: string): string;
7
+ /**
8
+ * Build manifest entries from scanned elements and output path
9
+ */
10
+ export declare function buildManifestEntries(elements: APIElement[], outputPath: string): ManifestEntry[];
11
+ /**
12
+ * Write manifest to .skrypt/manifest.json relative to output dir
13
+ */
14
+ export declare function writeManifest(outputPath: string, entries: ManifestEntry[]): void;
15
+ /**
16
+ * Read existing manifest
17
+ */
18
+ export declare function readManifest(outputPath: string): DocManifest | null;
@@ -0,0 +1,71 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { createHash } from 'crypto';
4
+ import { spawnSync } from 'child_process';
5
+ const MANIFEST_DIR = '.skrypt';
6
+ const MANIFEST_FILE = 'manifest.json';
7
+ /**
8
+ * Hash a signature string for change detection
9
+ */
10
+ export function hashSignature(signature) {
11
+ return createHash('sha256').update(signature).digest('hex').slice(0, 16);
12
+ }
13
+ /**
14
+ * Get current git commit SHA (short)
15
+ */
16
+ function getCommitSha() {
17
+ try {
18
+ const result = spawnSync('git', ['rev-parse', '--short', 'HEAD'], {
19
+ stdio: 'pipe',
20
+ timeout: 3000,
21
+ });
22
+ if (result.status === 0) {
23
+ return result.stdout.toString().trim();
24
+ }
25
+ }
26
+ catch {
27
+ // Not a git repo or git not available
28
+ }
29
+ return 'unknown';
30
+ }
31
+ /**
32
+ * Build manifest entries from scanned elements and output path
33
+ */
34
+ export function buildManifestEntries(elements, outputPath) {
35
+ return elements.map(el => ({
36
+ name: el.name,
37
+ kind: el.kind,
38
+ sourceFile: el.filePath,
39
+ signatureHash: hashSignature(el.signature),
40
+ docFile: outputPath,
41
+ }));
42
+ }
43
+ /**
44
+ * Write manifest to .skrypt/manifest.json relative to output dir
45
+ */
46
+ export function writeManifest(outputPath, entries) {
47
+ const manifestDir = join(outputPath, MANIFEST_DIR);
48
+ mkdirSync(manifestDir, { recursive: true });
49
+ const manifest = {
50
+ generatedAt: new Date().toISOString(),
51
+ commitSha: getCommitSha(),
52
+ elements: entries,
53
+ };
54
+ writeFileSync(join(manifestDir, MANIFEST_FILE), JSON.stringify(manifest, null, 2));
55
+ }
56
+ /**
57
+ * Read existing manifest
58
+ */
59
+ export function readManifest(outputPath) {
60
+ const manifestPath = join(outputPath, MANIFEST_DIR, MANIFEST_FILE);
61
+ if (!existsSync(manifestPath)) {
62
+ return null;
63
+ }
64
+ try {
65
+ const content = readFileSync(manifestPath, 'utf-8');
66
+ return JSON.parse(content);
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Replace a section in a markdown file that documents a specific element.
3
+ * Finds the heading for the element and replaces content until the next heading of same/higher level.
4
+ */
5
+ export declare function spliceDocSection(docFilePath: string, elementName: string, newContent: string): boolean;
6
+ /**
7
+ * Remove a section from a doc file for a removed element
8
+ */
9
+ export declare function removeDocSection(docFilePath: string, elementName: string): boolean;
@@ -0,0 +1,50 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'fs';
2
+ /**
3
+ * Replace a section in a markdown file that documents a specific element.
4
+ * Finds the heading for the element and replaces content until the next heading of same/higher level.
5
+ */
6
+ export function spliceDocSection(docFilePath, elementName, newContent) {
7
+ if (!existsSync(docFilePath)) {
8
+ return false;
9
+ }
10
+ const content = readFileSync(docFilePath, 'utf-8');
11
+ const lines = content.split('\n');
12
+ // Find the heading for this element
13
+ const headingPattern = new RegExp(`^(#{2,4})\\s+\`${escapeRegex(elementName)}\``);
14
+ let startIdx = -1;
15
+ let headingLevel = 0;
16
+ for (let i = 0; i < lines.length; i++) {
17
+ const match = lines[i].match(headingPattern);
18
+ if (match) {
19
+ startIdx = i;
20
+ headingLevel = match[1].length;
21
+ break;
22
+ }
23
+ }
24
+ if (startIdx === -1) {
25
+ return false;
26
+ }
27
+ // Find the end of this section (next heading of same or higher level)
28
+ let endIdx = lines.length;
29
+ for (let i = startIdx + 1; i < lines.length; i++) {
30
+ const nextHeading = lines[i].match(/^(#{2,4})\s/);
31
+ if (nextHeading && nextHeading[1].length <= headingLevel) {
32
+ endIdx = i;
33
+ break;
34
+ }
35
+ }
36
+ // Splice in new content
37
+ const newLines = newContent.split('\n');
38
+ lines.splice(startIdx, endIdx - startIdx, ...newLines);
39
+ writeFileSync(docFilePath, lines.join('\n'));
40
+ return true;
41
+ }
42
+ /**
43
+ * Remove a section from a doc file for a removed element
44
+ */
45
+ export function removeDocSection(docFilePath, elementName) {
46
+ return spliceDocSection(docFilePath, elementName, '');
47
+ }
48
+ function escapeRegex(str) {
49
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
50
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Entry in the doc manifest mapping code elements to docs
3
+ */
4
+ export interface ManifestEntry {
5
+ name: string;
6
+ kind: 'function' | 'class' | 'method';
7
+ sourceFile: string;
8
+ signatureHash: string;
9
+ docFile: string;
10
+ }
11
+ /**
12
+ * The full manifest written to .skrypt/manifest.json
13
+ */
14
+ export interface DocManifest {
15
+ generatedAt: string;
16
+ commitSha: string;
17
+ elements: ManifestEntry[];
18
+ }
19
+ /**
20
+ * An element whose signature has changed since last generation
21
+ */
22
+ export interface StaleElement {
23
+ entry: ManifestEntry;
24
+ newSignature: string;
25
+ newSignatureHash: string;
26
+ }
27
+ /**
28
+ * Report for a refresh operation
29
+ */
30
+ export interface RefreshReport {
31
+ regenerated: number;
32
+ removed: number;
33
+ unchanged: number;
34
+ total: number;
35
+ changedFiles: string[];
36
+ staleElements: StaleElement[];
37
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { LLMClient } from '../llm/types.js';
2
+ import { ReviewFeedback, ReviewResult } from './types.js';
3
+ export * from './types.js';
4
+ export * from './parser.js';
5
+ /**
6
+ * Apply a single feedback item by re-generating the relevant section
7
+ */
8
+ export declare function applyFeedback(feedback: ReviewFeedback, client: LLMClient): Promise<ReviewResult>;
@@ -0,0 +1,94 @@
1
+ import { readFileSync, writeFileSync } from 'fs';
2
+ export * from './types.js';
3
+ export * from './parser.js';
4
+ /**
5
+ * Apply a single feedback item by re-generating the relevant section
6
+ */
7
+ export async function applyFeedback(feedback, client) {
8
+ try {
9
+ const content = readFileSync(feedback.filePath, 'utf-8');
10
+ const lines = content.split('\n');
11
+ // Determine the section to rewrite
12
+ let sectionStart = 0;
13
+ let sectionEnd = lines.length;
14
+ if (feedback.lineNumber) {
15
+ // Find the heading before this line
16
+ for (let i = feedback.lineNumber - 1; i >= 0; i--) {
17
+ if (lines[i].match(/^#{2,4}\s/)) {
18
+ sectionStart = i;
19
+ break;
20
+ }
21
+ }
22
+ // Find next heading of same/higher level
23
+ const headingMatch = lines[sectionStart]?.match(/^(#{2,4})\s/);
24
+ const level = headingMatch ? headingMatch[1].length : 2;
25
+ for (let i = sectionStart + 1; i < lines.length; i++) {
26
+ const nextHeading = lines[i].match(/^(#{2,4})\s/);
27
+ if (nextHeading && nextHeading[1].length <= level) {
28
+ sectionEnd = i;
29
+ break;
30
+ }
31
+ }
32
+ }
33
+ else if (feedback.elementName) {
34
+ // Find the element heading
35
+ const pattern = new RegExp(`^#{2,4}\\s+\`${escapeRegex(feedback.elementName)}\``);
36
+ for (let i = 0; i < lines.length; i++) {
37
+ if (pattern.test(lines[i])) {
38
+ sectionStart = i;
39
+ const headingMatch = lines[i].match(/^(#{2,4})/);
40
+ const level = headingMatch ? headingMatch[1].length : 2;
41
+ for (let j = i + 1; j < lines.length; j++) {
42
+ const next = lines[j].match(/^(#{2,4})\s/);
43
+ if (next && next[1].length <= level) {
44
+ sectionEnd = j;
45
+ break;
46
+ }
47
+ }
48
+ break;
49
+ }
50
+ }
51
+ }
52
+ const section = lines.slice(sectionStart, sectionEnd).join('\n');
53
+ // Ask LLM to rewrite incorporating feedback
54
+ const response = await client.complete({
55
+ messages: [
56
+ {
57
+ role: 'system',
58
+ content: 'You are a technical documentation writer. Rewrite the given documentation section incorporating the provided feedback. Maintain the same heading level and markdown structure. Return ONLY the rewritten section, no explanation.'
59
+ },
60
+ {
61
+ role: 'user',
62
+ content: `## Current documentation section:\n\n${section}\n\n## Feedback to incorporate:\n\n${feedback.feedback}\n\n## Rewrite the section above incorporating this feedback:`
63
+ }
64
+ ],
65
+ temperature: 0,
66
+ maxTokens: 2048,
67
+ });
68
+ const newSection = response.content.trim();
69
+ if (!newSection) {
70
+ return { feedback, applied: false, error: 'LLM returned empty response' };
71
+ }
72
+ lines.splice(sectionStart, sectionEnd - sectionStart, ...newSection.split('\n'));
73
+ // Remove the inline comment if it was an inline feedback
74
+ if (feedback.source === 'inline') {
75
+ const updatedContent = lines.join('\n');
76
+ const cleaned = updatedContent.replace(/<!--\s*(?:FIXME|TODO)\s*:[\s\S]*?-->\s*\n?/g, '');
77
+ writeFileSync(feedback.filePath, cleaned);
78
+ }
79
+ else {
80
+ writeFileSync(feedback.filePath, lines.join('\n'));
81
+ }
82
+ return { feedback, applied: true };
83
+ }
84
+ catch (err) {
85
+ return {
86
+ feedback,
87
+ applied: false,
88
+ error: err instanceof Error ? err.message : String(err),
89
+ };
90
+ }
91
+ }
92
+ function escapeRegex(str) {
93
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
94
+ }
@@ -0,0 +1,16 @@
1
+ import { ReviewFeedback } from './types.js';
2
+ /**
3
+ * Parse a feedback markdown file.
4
+ *
5
+ * Format:
6
+ * ## docs/api/users.md
7
+ * - Line 42: Use the SDK client.get() method, not raw fetch()
8
+ * - General: Too formal, make it conversational
9
+ */
10
+ export declare function parseFeedbackFile(feedbackPath: string): ReviewFeedback[];
11
+ /**
12
+ * Scan markdown files for inline FIXME/TODO comments.
13
+ *
14
+ * Looks for: <!-- FIXME: ... --> or <!-- TODO: ... -->
15
+ */
16
+ export declare function parseInlineComments(docsDir: string): ReviewFeedback[];