Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/ai-tools/lib/auth-utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { execSync } from 'child_process'

/**
* Ensure GitHub token is available, exiting process if not found
* Falls back to the gh CLI token, setting process.env.GITHUB_TOKEN.
* Exits the process if neither is available.
*/
export function ensureGitHubToken(): void {
if (!process.env.GITHUB_TOKEN) {
Expand Down
6 changes: 0 additions & 6 deletions src/ai-tools/lib/call-models-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ export async function callModelsApi(
): Promise<string> {
let aiResponse: ChatCompletionChoice

// Set default model if none specified
if (!promptWithContent.model) {
promptWithContent.model = DEFAULT_MODEL
if (verbose) {
Expand All @@ -51,7 +50,6 @@ export async function callModelsApi(
}

try {
// Create an AbortController for timeout handling
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS)

Expand Down Expand Up @@ -82,7 +80,6 @@ export async function callModelsApi(
if (!response.ok) {
let errorMessage = `HTTP error! status: ${response.status} - ${response.statusText}`

// Try to get more detailed error information
try {
const errorBody = await response.json()
if (errorBody.error && errorBody.error.message) {
Expand All @@ -92,7 +89,6 @@ export async function callModelsApi(
// If we can't parse error body, continue with basic error
}

// Add helpful hints for common errors
if (response.status === 401) {
errorMessage += ' (Check your GITHUB_TOKEN)'
} else if (response.status === 400) {
Expand Down Expand Up @@ -135,9 +131,7 @@ export async function callModelsApi(
return cleanAIResponse(aiResponse.message.content)
}

// Helper function to clean up AI response content
function cleanAIResponse(content: string): string {
// Remove markdown code blocks
return content
.replace(/^```[\w]*\n/gm, '') // Remove opening code blocks
.replace(/\n```$/gm, '') // Remove closing code blocks at end
Expand Down
17 changes: 3 additions & 14 deletions src/ai-tools/lib/file-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,6 @@ import { schema } from '@/frame/lib/frontmatter'

const MAX_DIRECTORY_DEPTH = 20

/**
* Enhanced recursive markdown file finder with symlink, depth, and root path checks
*/
export function findMarkdownFiles(
dir: string,
rootDir: string,
Expand All @@ -33,7 +30,6 @@ export function findMarkdownFiles(
return []
}
visited.add(realDir)
// Prevent excessive depth
if (depth > maxDepth) {
return []
}
Expand Down Expand Up @@ -71,9 +67,8 @@ interface FrontmatterProperties {
}

/**
* Function to merge new frontmatter properties into existing file while preserving formatting.
* Uses surgical replacement to only modify the specific field(s) being updated,
* preserving all original YAML formatting for unchanged fields.
* Replaces only the fields being updated, so the original YAML formatting
* of every other field survives.
*/
export function mergeFrontmatterProperties(filePath: string, newPropertiesYaml: string): string {
const content = fs.readFileSync(filePath, 'utf8')
Expand All @@ -90,7 +85,7 @@ export function mergeFrontmatterProperties(filePath: string, newPropertiesYaml:
}

try {
// Clean up the AI response - remove markdown code blocks if present
// The model often wraps its output in a code fence.
let cleanedYaml = newPropertiesYaml.trim()
cleanedYaml = cleanedYaml.replace(/^```ya?ml\s*\n/i, '')
cleanedYaml = cleanedYaml.replace(/\n```\s*$/i, '')
Expand All @@ -111,12 +106,10 @@ export function mergeFrontmatterProperties(filePath: string, newPropertiesYaml:
}),
)

// Split content into lines for surgical replacement
const lines = content.split('\n')
let inFrontmatter = false
let frontmatterEndIndex = -1

// Find frontmatter boundaries
for (let i = 0; i < lines.length; i++) {
if (lines[i].trim() === '---') {
if (!inFrontmatter) {
Expand All @@ -128,17 +121,14 @@ export function mergeFrontmatterProperties(filePath: string, newPropertiesYaml:
}
}

// Replace each field value while preserving everything else
for (const [key, value] of Object.entries(sanitizedProperties)) {
const formattedValue = typeof value === 'string' ? `'${value.replace(/'/g, "''")}'` : value

// Find the line with this field
let foundField = false
for (let i = 1; i < frontmatterEndIndex; i++) {
const line = lines[i]
if (line.startsWith(`${key}:`)) {
foundField = true
// Simple replacement: keep the field name and spacing, replace the value
const colonIndex = line.indexOf(':')
const leadingSpace = line.substring(colonIndex + 1, colonIndex + 2) // Usually a space
lines[i] = `${key}:${leadingSpace}${formattedValue}`
Expand All @@ -153,7 +143,6 @@ export function mergeFrontmatterProperties(filePath: string, newPropertiesYaml:
}
}

// If field doesn't exist, add it before the closing ---
if (!foundField && frontmatterEndIndex > 0) {
lines.splice(frontmatterEndIndex, 0, `${key}: ${formattedValue}`)
frontmatterEndIndex++
Expand Down
26 changes: 3 additions & 23 deletions src/ai-tools/lib/prompt-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,11 @@ export interface PromptData {
max_tokens?: number
}

/**
* Get the prompts directory path
*/
export function getPromptsDir(): string {
const __dirname = path.dirname(fileURLToPath(import.meta.url))
return path.join(__dirname, '../prompts')
}

/**
* Dynamically discover available editor types from prompt files
*/
export function getAvailableEditorTypes(promptDir: string): string[] {
const editorTypes: string[] = []

Expand All @@ -46,16 +40,10 @@ export function getAvailableEditorTypes(promptDir: string): string[] {
return editorTypes
}

/**
* Get formatted description of available refinement types
*/
export function getRefinementDescriptions(editorTypes: string[]): string {
return editorTypes.join(', ')
}

/**
* Enrich context for intro prompt on index.md files
*/
export function enrichIndexContext(filePath: string, content: string): string {
if (!filePath.endsWith('index.md')) return content

Expand All @@ -72,7 +60,6 @@ export function enrichIndexContext(filePath: string, content: string): string {
.join(' ')
: ''

// Get child article titles
const titles: string[] = []
if (data.children && Array.isArray(data.children)) {
const dir = path.dirname(filePath)
Expand All @@ -94,7 +81,6 @@ export function enrichIndexContext(filePath: string, content: string): string {
}
}

// Build context note
const parts: string[] = []
if (productName) parts.push(`Product: ${productName}`)
if (titles.length > 0) parts.push(`Child articles: ${titles.join(', ')}`)
Expand All @@ -114,24 +100,19 @@ export function enrichIndexContext(filePath: string, content: string): string {
return content
}

/**
* Call an editor with the given content and options
*/
export async function callEditor(
editorType: string,
content: string,
promptDir: string,
writeMode: boolean,
verbose = false,
promptContent?: string, // Optional: use this instead of reading from file
promptContent?: string, // Use this instead of reading a prompt file
): Promise<string> {
let markdownPrompt: string

if (promptContent) {
// Use provided prompt content (e.g., from Copilot Space)
markdownPrompt = promptContent
} else {
// Read from file
const markdownPromptPath = path.join(promptDir, `${editorType}.md`)

if (!fs.existsSync(markdownPromptPath)) {
Expand All @@ -144,15 +125,15 @@ export async function callEditor(

const prompt = load(fs.readFileSync(promptTemplatePath, 'utf8')) as PromptData

// Validate the prompt template has required properties
if (!prompt.messages || !Array.isArray(prompt.messages)) {
throw new Error('Invalid prompt template: missing or invalid messages array')
}

for (const msg of prompt.messages) {
msg.content = msg.content.replace('{{markdownPrompt}}', markdownPrompt)
msg.content = msg.content.replace('{{input}}', content)
// Replace writeMode template variable with simple string replacement
// Resolve the write-mode markers, then strip whatever they marked
// for removal.
msg.content = msg.content.replace(
/<!-- IF_WRITE_MODE -->/g,
writeMode ? '' : '<!-- REMOVE_START -->',
Expand All @@ -166,7 +147,6 @@ export async function callEditor(
writeMode ? '' : '<!-- REMOVE_END -->',
)

// Remove sections marked for removal
msg.content = msg.content.replace(/<!-- REMOVE_START -->[\s\S]*?<!-- REMOVE_END -->/g, '')
}

Expand Down
15 changes: 0 additions & 15 deletions src/ai-tools/lib/spaces-utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
/**
* Copilot Space API response types
*/
import { fetchWithRetry, readBodyWithTimeout } from '@/frame/lib/fetch-utils'

export interface SpaceResource {
Expand All @@ -25,9 +22,6 @@ export interface SpaceData {
updated_at: string
}

/**
* Parse a Copilot Space URL to extract org and space ID
*/
export function parseSpaceUrl(url: string): { org: string; id: string } {
// Expected format: https://api.github.com/orgs/{org}/copilot-spaces/{id}
const match = url.match(/\/orgs\/([^/]+)\/copilot-spaces\/(\d+)/)
Expand All @@ -44,9 +38,6 @@ export function parseSpaceUrl(url: string): { org: string; id: string } {
}
}

/**
* Fetch a Copilot Space from the GitHub API
*/
export async function fetchCopilotSpace(spaceUrl: string): Promise<SpaceData> {
const { org, id } = parseSpaceUrl(spaceUrl)
const apiUrl = `https://api.github.com/orgs/${org}/copilot-spaces/${id}`
Expand Down Expand Up @@ -88,26 +79,20 @@ export async function fetchCopilotSpace(spaceUrl: string): Promise<SpaceData> {
return (await readBodyWithTimeout(response, () => response.json(), 30_000)) as SpaceData
}

/**
* Convert a Copilot Space to a markdown prompt file
*/
export function convertSpaceToPrompt(space: SpaceData): string {
const timestamp = new Date().toISOString()
const lines: string[] = []

// Header with metadata
lines.push(`<!-- Generated from Copilot Space: ${space.name} -->`)
lines.push(`<!-- Space ID: ${space.number} | Generated: ${timestamp} -->`)
lines.push(`<!-- Space URL: ${space.html_url} -->`)
lines.push('')

// General instructions
if (space.general_instructions) {
lines.push(space.general_instructions.trim())
lines.push('')
}

// Add each resource as a context section
if (space.resources_attributes && space.resources_attributes.length > 0) {
for (const resource of space.resources_attributes) {
if (resource.resource_type === 'free_text' && resource.metadata) {
Expand Down
Loading
Loading