mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-02 20:43:36 +00:00
Merge main into kanban-scaling
Resolves merge conflicts while preserving: - Kanban scaling improvements (window sizing, bounce prevention, debouncing) - Main's sidebar refactoring into hooks - Main's openInEditor functionality for VS Code integration 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
20
.github/actions/setup-project/action.yml
vendored
20
.github/actions/setup-project/action.yml
vendored
@@ -1,28 +1,28 @@
|
|||||||
name: "Setup Project"
|
name: 'Setup Project'
|
||||||
description: "Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules"
|
description: 'Common setup steps for CI workflows - checkout, Node.js, dependencies, and native modules'
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
node-version:
|
node-version:
|
||||||
description: "Node.js version to use"
|
description: 'Node.js version to use'
|
||||||
required: false
|
required: false
|
||||||
default: "22"
|
default: '22'
|
||||||
check-lockfile:
|
check-lockfile:
|
||||||
description: "Run lockfile lint check for SSH URLs"
|
description: 'Run lockfile lint check for SSH URLs'
|
||||||
required: false
|
required: false
|
||||||
default: "false"
|
default: 'false'
|
||||||
rebuild-node-pty-path:
|
rebuild-node-pty-path:
|
||||||
description: "Working directory for node-pty rebuild (empty = root)"
|
description: 'Working directory for node-pty rebuild (empty = root)'
|
||||||
required: false
|
required: false
|
||||||
default: ""
|
default: ''
|
||||||
|
|
||||||
runs:
|
runs:
|
||||||
using: "composite"
|
using: 'composite'
|
||||||
steps:
|
steps:
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ inputs.node-version }}
|
node-version: ${{ inputs.node-version }}
|
||||||
cache: "npm"
|
cache: 'npm'
|
||||||
cache-dependency-path: package-lock.json
|
cache-dependency-path: package-lock.json
|
||||||
|
|
||||||
- name: Check for SSH URLs in lockfile
|
- name: Check for SSH URLs in lockfile
|
||||||
|
|||||||
179
.github/scripts/upload-to-r2.js
vendored
179
.github/scripts/upload-to-r2.js
vendored
@@ -1,15 +1,11 @@
|
|||||||
const {
|
const { S3Client, PutObjectCommand, GetObjectCommand } = require('@aws-sdk/client-s3');
|
||||||
S3Client,
|
const fs = require('fs');
|
||||||
PutObjectCommand,
|
const path = require('path');
|
||||||
GetObjectCommand,
|
const https = require('https');
|
||||||
} = require("@aws-sdk/client-s3");
|
const { pipeline } = require('stream/promises');
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
const https = require("https");
|
|
||||||
const { pipeline } = require("stream/promises");
|
|
||||||
|
|
||||||
const s3Client = new S3Client({
|
const s3Client = new S3Client({
|
||||||
region: "auto",
|
region: 'auto',
|
||||||
endpoint: process.env.R2_ENDPOINT,
|
endpoint: process.env.R2_ENDPOINT,
|
||||||
credentials: {
|
credentials: {
|
||||||
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
accessKeyId: process.env.R2_ACCESS_KEY_ID,
|
||||||
@@ -28,14 +24,14 @@ async function fetchExistingReleases() {
|
|||||||
const response = await s3Client.send(
|
const response = await s3Client.send(
|
||||||
new GetObjectCommand({
|
new GetObjectCommand({
|
||||||
Bucket: BUCKET,
|
Bucket: BUCKET,
|
||||||
Key: "releases.json",
|
Key: 'releases.json',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const body = await response.Body.transformToString();
|
const body = await response.Body.transformToString();
|
||||||
return JSON.parse(body);
|
return JSON.parse(body);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === "NoSuchKey" || error.$metadata?.httpStatusCode === 404) {
|
if (error.name === 'NoSuchKey' || error.$metadata?.httpStatusCode === 404) {
|
||||||
console.log("No existing releases.json found, creating new one");
|
console.log('No existing releases.json found, creating new one');
|
||||||
return { latestVersion: null, releases: [] };
|
return { latestVersion: null, releases: [] };
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
@@ -85,7 +81,7 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
|
|||||||
resolve({
|
resolve({
|
||||||
accessible: false,
|
accessible: false,
|
||||||
statusCode,
|
statusCode,
|
||||||
error: "Redirect without location header",
|
error: 'Redirect without location header',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -93,18 +89,16 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
|
|||||||
return https
|
return https
|
||||||
.get(redirectUrl, { timeout: 10000 }, (redirectResponse) => {
|
.get(redirectUrl, { timeout: 10000 }, (redirectResponse) => {
|
||||||
const redirectStatus = redirectResponse.statusCode;
|
const redirectStatus = redirectResponse.statusCode;
|
||||||
const contentType =
|
const contentType = redirectResponse.headers['content-type'] || '';
|
||||||
redirectResponse.headers["content-type"] || "";
|
|
||||||
// Check if it's actually a file (zip/tar.gz) and not HTML
|
// Check if it's actually a file (zip/tar.gz) and not HTML
|
||||||
const isFile =
|
const isFile =
|
||||||
contentType.includes("application/zip") ||
|
contentType.includes('application/zip') ||
|
||||||
contentType.includes("application/gzip") ||
|
contentType.includes('application/gzip') ||
|
||||||
contentType.includes("application/x-gzip") ||
|
contentType.includes('application/x-gzip') ||
|
||||||
contentType.includes("application/x-tar") ||
|
contentType.includes('application/x-tar') ||
|
||||||
redirectUrl.includes(".zip") ||
|
redirectUrl.includes('.zip') ||
|
||||||
redirectUrl.includes(".tar.gz");
|
redirectUrl.includes('.tar.gz');
|
||||||
const isGood =
|
const isGood = redirectStatus >= 200 && redirectStatus < 300 && isFile;
|
||||||
redirectStatus >= 200 && redirectStatus < 300 && isFile;
|
|
||||||
redirectResponse.destroy();
|
redirectResponse.destroy();
|
||||||
resolve({
|
resolve({
|
||||||
accessible: isGood,
|
accessible: isGood,
|
||||||
@@ -113,38 +107,38 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
|
|||||||
contentType,
|
contentType,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.on("error", (error) => {
|
.on('error', (error) => {
|
||||||
resolve({
|
resolve({
|
||||||
accessible: false,
|
accessible: false,
|
||||||
statusCode,
|
statusCode,
|
||||||
error: error.message,
|
error: error.message,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.on("timeout", function () {
|
.on('timeout', function () {
|
||||||
this.destroy();
|
this.destroy();
|
||||||
resolve({
|
resolve({
|
||||||
accessible: false,
|
accessible: false,
|
||||||
statusCode,
|
statusCode,
|
||||||
error: "Timeout following redirect",
|
error: 'Timeout following redirect',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if status is good (200-299 range) and it's actually a file
|
// Check if status is good (200-299 range) and it's actually a file
|
||||||
const contentType = response.headers["content-type"] || "";
|
const contentType = response.headers['content-type'] || '';
|
||||||
const isFile =
|
const isFile =
|
||||||
contentType.includes("application/zip") ||
|
contentType.includes('application/zip') ||
|
||||||
contentType.includes("application/gzip") ||
|
contentType.includes('application/gzip') ||
|
||||||
contentType.includes("application/x-gzip") ||
|
contentType.includes('application/x-gzip') ||
|
||||||
contentType.includes("application/x-tar") ||
|
contentType.includes('application/x-tar') ||
|
||||||
url.includes(".zip") ||
|
url.includes('.zip') ||
|
||||||
url.includes(".tar.gz");
|
url.includes('.tar.gz');
|
||||||
const isGood = statusCode >= 200 && statusCode < 300 && isFile;
|
const isGood = statusCode >= 200 && statusCode < 300 && isFile;
|
||||||
response.destroy();
|
response.destroy();
|
||||||
resolve({ accessible: isGood, statusCode, contentType });
|
resolve({ accessible: isGood, statusCode, contentType });
|
||||||
});
|
});
|
||||||
|
|
||||||
request.on("error", (error) => {
|
request.on('error', (error) => {
|
||||||
resolve({
|
resolve({
|
||||||
accessible: false,
|
accessible: false,
|
||||||
statusCode: null,
|
statusCode: null,
|
||||||
@@ -152,12 +146,12 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
request.on("timeout", () => {
|
request.on('timeout', () => {
|
||||||
request.destroy();
|
request.destroy();
|
||||||
resolve({
|
resolve({
|
||||||
accessible: false,
|
accessible: false,
|
||||||
statusCode: null,
|
statusCode: null,
|
||||||
error: "Request timeout",
|
error: 'Request timeout',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -168,22 +162,14 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
|
|||||||
`✓ URL ${url} is now accessible after ${attempt} retries (status: ${result.statusCode})`
|
`✓ URL ${url} is now accessible after ${attempt} retries (status: ${result.statusCode})`
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(`✓ URL ${url} is accessible (status: ${result.statusCode})`);
|
||||||
`✓ URL ${url} is accessible (status: ${result.statusCode})`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return result.finalUrl || url; // Return the final URL (after redirects) if available
|
return result.finalUrl || url; // Return the final URL (after redirects) if available
|
||||||
} else {
|
} else {
|
||||||
const errorMsg = result.error ? ` - ${result.error}` : "";
|
const errorMsg = result.error ? ` - ${result.error}` : '';
|
||||||
const statusMsg = result.statusCode
|
const statusMsg = result.statusCode ? ` (status: ${result.statusCode})` : '';
|
||||||
? ` (status: ${result.statusCode})`
|
const contentTypeMsg = result.contentType ? ` [content-type: ${result.contentType}]` : '';
|
||||||
: "";
|
console.log(`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`);
|
||||||
const contentTypeMsg = result.contentType
|
|
||||||
? ` [content-type: ${result.contentType}]`
|
|
||||||
: "";
|
|
||||||
console.log(
|
|
||||||
`✗ URL ${url} not accessible${statusMsg}${contentTypeMsg}${errorMsg}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(`✗ URL ${url} check failed: ${error.message}`);
|
console.log(`✗ URL ${url} check failed: ${error.message}`);
|
||||||
@@ -191,9 +177,7 @@ async function checkUrlAccessible(url, maxRetries = 10, initialDelay = 1000) {
|
|||||||
|
|
||||||
if (attempt < maxRetries - 1) {
|
if (attempt < maxRetries - 1) {
|
||||||
const delay = initialDelay * Math.pow(2, attempt);
|
const delay = initialDelay * Math.pow(2, attempt);
|
||||||
console.log(
|
console.log(` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`);
|
||||||
` Retrying in ${delay}ms... (attempt ${attempt + 1}/${maxRetries})`
|
|
||||||
);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, delay));
|
await new Promise((resolve) => setTimeout(resolve, delay));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -207,12 +191,7 @@ async function downloadFromGitHub(url, outputPath) {
|
|||||||
const statusCode = response.statusCode;
|
const statusCode = response.statusCode;
|
||||||
|
|
||||||
// Follow redirects (all redirect types)
|
// Follow redirects (all redirect types)
|
||||||
if (
|
if (statusCode === 301 || statusCode === 302 || statusCode === 307 || statusCode === 308) {
|
||||||
statusCode === 301 ||
|
|
||||||
statusCode === 302 ||
|
|
||||||
statusCode === 307 ||
|
|
||||||
statusCode === 308
|
|
||||||
) {
|
|
||||||
const redirectUrl = response.headers.location;
|
const redirectUrl = response.headers.location;
|
||||||
response.destroy();
|
response.destroy();
|
||||||
if (!redirectUrl) {
|
if (!redirectUrl) {
|
||||||
@@ -220,39 +199,33 @@ async function downloadFromGitHub(url, outputPath) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Resolve relative redirects
|
// Resolve relative redirects
|
||||||
const finalRedirectUrl = redirectUrl.startsWith("http")
|
const finalRedirectUrl = redirectUrl.startsWith('http')
|
||||||
? redirectUrl
|
? redirectUrl
|
||||||
: new URL(redirectUrl, url).href;
|
: new URL(redirectUrl, url).href;
|
||||||
console.log(` Following redirect: ${finalRedirectUrl}`);
|
console.log(` Following redirect: ${finalRedirectUrl}`);
|
||||||
return downloadFromGitHub(finalRedirectUrl, outputPath)
|
return downloadFromGitHub(finalRedirectUrl, outputPath).then(resolve).catch(reject);
|
||||||
.then(resolve)
|
|
||||||
.catch(reject);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusCode !== 200) {
|
if (statusCode !== 200) {
|
||||||
response.destroy();
|
response.destroy();
|
||||||
reject(
|
reject(new Error(`Failed to download ${url}: ${statusCode} ${response.statusMessage}`));
|
||||||
new Error(
|
|
||||||
`Failed to download ${url}: ${statusCode} ${response.statusMessage}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileStream = fs.createWriteStream(outputPath);
|
const fileStream = fs.createWriteStream(outputPath);
|
||||||
response.pipe(fileStream);
|
response.pipe(fileStream);
|
||||||
fileStream.on("finish", () => {
|
fileStream.on('finish', () => {
|
||||||
fileStream.close();
|
fileStream.close();
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
fileStream.on("error", (error) => {
|
fileStream.on('error', (error) => {
|
||||||
response.destroy();
|
response.destroy();
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
request.on("error", reject);
|
request.on('error', reject);
|
||||||
request.on("timeout", () => {
|
request.on('timeout', () => {
|
||||||
request.destroy();
|
request.destroy();
|
||||||
reject(new Error(`Request timeout for ${url}`));
|
reject(new Error(`Request timeout for ${url}`));
|
||||||
});
|
});
|
||||||
@@ -260,8 +233,8 @@ async function downloadFromGitHub(url, outputPath) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const artifactsDir = "artifacts";
|
const artifactsDir = 'artifacts';
|
||||||
const tempDir = path.join(artifactsDir, "temp");
|
const tempDir = path.join(artifactsDir, 'temp');
|
||||||
|
|
||||||
// Create temp directory for downloaded GitHub archives
|
// Create temp directory for downloaded GitHub archives
|
||||||
if (!fs.existsSync(tempDir)) {
|
if (!fs.existsSync(tempDir)) {
|
||||||
@@ -292,40 +265,30 @@ async function main() {
|
|||||||
|
|
||||||
// Find all artifacts
|
// Find all artifacts
|
||||||
const artifacts = {
|
const artifacts = {
|
||||||
windows: findArtifacts(path.join(artifactsDir, "windows-builds"), /\.exe$/),
|
windows: findArtifacts(path.join(artifactsDir, 'windows-builds'), /\.exe$/),
|
||||||
macos: findArtifacts(path.join(artifactsDir, "macos-builds"), /-x64\.dmg$/),
|
macos: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-x64\.dmg$/),
|
||||||
macosArm: findArtifacts(
|
macosArm: findArtifacts(path.join(artifactsDir, 'macos-builds'), /-arm64\.dmg$/),
|
||||||
path.join(artifactsDir, "macos-builds"),
|
linux: findArtifacts(path.join(artifactsDir, 'linux-builds'), /\.AppImage$/),
|
||||||
/-arm64\.dmg$/
|
|
||||||
),
|
|
||||||
linux: findArtifacts(
|
|
||||||
path.join(artifactsDir, "linux-builds"),
|
|
||||||
/\.AppImage$/
|
|
||||||
),
|
|
||||||
sourceZip: [sourceZipPath],
|
sourceZip: [sourceZipPath],
|
||||||
sourceTarGz: [sourceTarGzPath],
|
sourceTarGz: [sourceTarGzPath],
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("Found artifacts:");
|
console.log('Found artifacts:');
|
||||||
for (const [platform, files] of Object.entries(artifacts)) {
|
for (const [platform, files] of Object.entries(artifacts)) {
|
||||||
console.log(
|
console.log(
|
||||||
` ${platform}: ${
|
` ${platform}: ${files.length > 0 ? files.map((f) => path.basename(f)).join(', ') : 'none'}`
|
||||||
files.length > 0
|
|
||||||
? files.map((f) => path.basename(f)).join(", ")
|
|
||||||
: "none"
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload each artifact to R2
|
// Upload each artifact to R2
|
||||||
const assets = {};
|
const assets = {};
|
||||||
const contentTypes = {
|
const contentTypes = {
|
||||||
windows: "application/x-msdownload",
|
windows: 'application/x-msdownload',
|
||||||
macos: "application/x-apple-diskimage",
|
macos: 'application/x-apple-diskimage',
|
||||||
macosArm: "application/x-apple-diskimage",
|
macosArm: 'application/x-apple-diskimage',
|
||||||
linux: "application/x-executable",
|
linux: 'application/x-executable',
|
||||||
sourceZip: "application/zip",
|
sourceZip: 'application/zip',
|
||||||
sourceTarGz: "application/gzip",
|
sourceTarGz: 'application/gzip',
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const [platform, files] of Object.entries(artifacts)) {
|
for (const [platform, files] of Object.entries(artifacts)) {
|
||||||
@@ -345,11 +308,11 @@ async function main() {
|
|||||||
filename,
|
filename,
|
||||||
size,
|
size,
|
||||||
arch:
|
arch:
|
||||||
platform === "macosArm"
|
platform === 'macosArm'
|
||||||
? "arm64"
|
? 'arm64'
|
||||||
: platform === "sourceZip" || platform === "sourceTarGz"
|
: platform === 'sourceZip' || platform === 'sourceTarGz'
|
||||||
? "source"
|
? 'source'
|
||||||
: "x64",
|
: 'x64',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -364,9 +327,7 @@ async function main() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Remove existing entry for this version if re-running
|
// Remove existing entry for this version if re-running
|
||||||
releasesData.releases = releasesData.releases.filter(
|
releasesData.releases = releasesData.releases.filter((r) => r.version !== VERSION);
|
||||||
(r) => r.version !== VERSION
|
|
||||||
);
|
|
||||||
|
|
||||||
// Prepend new release
|
// Prepend new release
|
||||||
releasesData.releases.unshift(newRelease);
|
releasesData.releases.unshift(newRelease);
|
||||||
@@ -376,19 +337,19 @@ async function main() {
|
|||||||
await s3Client.send(
|
await s3Client.send(
|
||||||
new PutObjectCommand({
|
new PutObjectCommand({
|
||||||
Bucket: BUCKET,
|
Bucket: BUCKET,
|
||||||
Key: "releases.json",
|
Key: 'releases.json',
|
||||||
Body: JSON.stringify(releasesData, null, 2),
|
Body: JSON.stringify(releasesData, null, 2),
|
||||||
ContentType: "application/json",
|
ContentType: 'application/json',
|
||||||
CacheControl: "public, max-age=60",
|
CacheControl: 'public, max-age=60',
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log("Successfully updated releases.json");
|
console.log('Successfully updated releases.json');
|
||||||
console.log(`Latest version: ${VERSION}`);
|
console.log(`Latest version: ${VERSION}`);
|
||||||
console.log(`Total releases: ${releasesData.releases.length}`);
|
console.log(`Total releases: ${releasesData.releases.length}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
main().catch((err) => {
|
main().catch((err) => {
|
||||||
console.error("Failed to upload to R2:", err);
|
console.error('Failed to upload to R2:', err);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|||||||
1
.github/workflows/claude.yml
vendored
1
.github/workflows/claude.yml
vendored
@@ -47,4 +47,3 @@ jobs:
|
|||||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||||
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
# claude_args: '--allowed-tools Bash(gh pr:*)'
|
||||||
|
|
||||||
|
|||||||
8
.github/workflows/e2e-tests.yml
vendored
8
.github/workflows/e2e-tests.yml
vendored
@@ -3,7 +3,7 @@ name: E2E Tests
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "*"
|
- '*'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -21,8 +21,8 @@ jobs:
|
|||||||
- name: Setup project
|
- name: Setup project
|
||||||
uses: ./.github/actions/setup-project
|
uses: ./.github/actions/setup-project
|
||||||
with:
|
with:
|
||||||
check-lockfile: "true"
|
check-lockfile: 'true'
|
||||||
rebuild-node-pty-path: "apps/server"
|
rebuild-node-pty-path: 'apps/server'
|
||||||
|
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
run: npx playwright install --with-deps chromium
|
run: npx playwright install --with-deps chromium
|
||||||
@@ -58,7 +58,7 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
CI: true
|
CI: true
|
||||||
VITE_SERVER_URL: http://localhost:3008
|
VITE_SERVER_URL: http://localhost:3008
|
||||||
VITE_SKIP_SETUP: "true"
|
VITE_SKIP_SETUP: 'true'
|
||||||
|
|
||||||
- name: Upload Playwright report
|
- name: Upload Playwright report
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
|||||||
4
.github/workflows/pr-check.yml
vendored
4
.github/workflows/pr-check.yml
vendored
@@ -3,7 +3,7 @@ name: PR Build Check
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "*"
|
- '*'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -20,7 +20,7 @@ jobs:
|
|||||||
- name: Setup project
|
- name: Setup project
|
||||||
uses: ./.github/actions/setup-project
|
uses: ./.github/actions/setup-project
|
||||||
with:
|
with:
|
||||||
check-lockfile: "true"
|
check-lockfile: 'true'
|
||||||
|
|
||||||
- name: Run build:electron (dir only - faster CI)
|
- name: Run build:electron (dir only - faster CI)
|
||||||
run: npm run build:electron:dir
|
run: npm run build:electron:dir
|
||||||
|
|||||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -3,7 +3,7 @@ name: Test Suite
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- "*"
|
- '*'
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
@@ -20,8 +20,8 @@ jobs:
|
|||||||
- name: Setup project
|
- name: Setup project
|
||||||
uses: ./.github/actions/setup-project
|
uses: ./.github/actions/setup-project
|
||||||
with:
|
with:
|
||||||
check-lockfile: "true"
|
check-lockfile: 'true'
|
||||||
rebuild-node-pty-path: "apps/server"
|
rebuild-node-pty-path: 'apps/server'
|
||||||
|
|
||||||
- name: Run package tests
|
- name: Run package tests
|
||||||
run: npm run test:packages
|
run: npm run test:packages
|
||||||
|
|||||||
2
apps/app/next-env.d.ts
vendored
2
apps/app/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
|||||||
/// <reference types="next" />
|
/// <reference types="next" />
|
||||||
/// <reference types="next/image-types/global" />
|
/// <reference types="next/image-types/global" />
|
||||||
import "./.next/dev/types/routes.d.ts";
|
import './.next/dev/types/routes.d.ts';
|
||||||
|
|
||||||
// NOTE: This file should not be edited
|
// NOTE: This file should not be edited
|
||||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
|
|||||||
@@ -6,53 +6,54 @@
|
|||||||
* In web mode, this server runs on a remote host.
|
* In web mode, this server runs on a remote host.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import express from "express";
|
import express from 'express';
|
||||||
import cors from "cors";
|
import cors from 'cors';
|
||||||
import morgan from "morgan";
|
import morgan from 'morgan';
|
||||||
import { WebSocketServer, WebSocket } from "ws";
|
import { WebSocketServer, WebSocket } from 'ws';
|
||||||
import { createServer } from "http";
|
import { createServer } from 'http';
|
||||||
import dotenv from "dotenv";
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
import { createEventEmitter, type EventEmitter } from "./lib/events.js";
|
import { createEventEmitter, type EventEmitter } from './lib/events.js';
|
||||||
import { initAllowedPaths } from "@automaker/platform";
|
import { initAllowedPaths } from '@automaker/platform';
|
||||||
import { authMiddleware, getAuthStatus } from "./lib/auth.js";
|
import { authMiddleware, getAuthStatus } from './lib/auth.js';
|
||||||
import { createFsRoutes } from "./routes/fs/index.js";
|
import { createFsRoutes } from './routes/fs/index.js';
|
||||||
import { createHealthRoutes } from "./routes/health/index.js";
|
import { createHealthRoutes } from './routes/health/index.js';
|
||||||
import { createAgentRoutes } from "./routes/agent/index.js";
|
import { createAgentRoutes } from './routes/agent/index.js';
|
||||||
import { createSessionsRoutes } from "./routes/sessions/index.js";
|
import { createSessionsRoutes } from './routes/sessions/index.js';
|
||||||
import { createFeaturesRoutes } from "./routes/features/index.js";
|
import { createFeaturesRoutes } from './routes/features/index.js';
|
||||||
import { createAutoModeRoutes } from "./routes/auto-mode/index.js";
|
import { createAutoModeRoutes } from './routes/auto-mode/index.js';
|
||||||
import { createEnhancePromptRoutes } from "./routes/enhance-prompt/index.js";
|
import { createEnhancePromptRoutes } from './routes/enhance-prompt/index.js';
|
||||||
import { createWorktreeRoutes } from "./routes/worktree/index.js";
|
import { createWorktreeRoutes } from './routes/worktree/index.js';
|
||||||
import { createGitRoutes } from "./routes/git/index.js";
|
import { createGitRoutes } from './routes/git/index.js';
|
||||||
import { createSetupRoutes } from "./routes/setup/index.js";
|
import { createSetupRoutes } from './routes/setup/index.js';
|
||||||
import { createSuggestionsRoutes } from "./routes/suggestions/index.js";
|
import { createSuggestionsRoutes } from './routes/suggestions/index.js';
|
||||||
import { createModelsRoutes } from "./routes/models/index.js";
|
import { createModelsRoutes } from './routes/models/index.js';
|
||||||
import { createRunningAgentsRoutes } from "./routes/running-agents/index.js";
|
import { createRunningAgentsRoutes } from './routes/running-agents/index.js';
|
||||||
import { createWorkspaceRoutes } from "./routes/workspace/index.js";
|
import { createWorkspaceRoutes } from './routes/workspace/index.js';
|
||||||
import { createTemplatesRoutes } from "./routes/templates/index.js";
|
import { createTemplatesRoutes } from './routes/templates/index.js';
|
||||||
import {
|
import {
|
||||||
createTerminalRoutes,
|
createTerminalRoutes,
|
||||||
validateTerminalToken,
|
validateTerminalToken,
|
||||||
isTerminalEnabled,
|
isTerminalEnabled,
|
||||||
isTerminalPasswordRequired,
|
isTerminalPasswordRequired,
|
||||||
} from "./routes/terminal/index.js";
|
} from './routes/terminal/index.js';
|
||||||
import { createSettingsRoutes } from "./routes/settings/index.js";
|
import { createSettingsRoutes } from './routes/settings/index.js';
|
||||||
import { AgentService } from "./services/agent-service.js";
|
import { AgentService } from './services/agent-service.js';
|
||||||
import { FeatureLoader } from "./services/feature-loader.js";
|
import { FeatureLoader } from './services/feature-loader.js';
|
||||||
import { AutoModeService } from "./services/auto-mode-service.js";
|
import { AutoModeService } from './services/auto-mode-service.js';
|
||||||
import { getTerminalService } from "./services/terminal-service.js";
|
import { getTerminalService } from './services/terminal-service.js';
|
||||||
import { SettingsService } from "./services/settings-service.js";
|
import { SettingsService } from './services/settings-service.js';
|
||||||
import { createSpecRegenerationRoutes } from "./routes/app-spec/index.js";
|
import { createSpecRegenerationRoutes } from './routes/app-spec/index.js';
|
||||||
import { createClaudeRoutes } from "./routes/claude/index.js";
|
import { createClaudeRoutes } from './routes/claude/index.js';
|
||||||
import { ClaudeUsageService } from "./services/claude-usage-service.js";
|
import { ClaudeUsageService } from './services/claude-usage-service.js';
|
||||||
|
import { createContextRoutes } from './routes/context/index.js';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || "3008", 10);
|
const PORT = parseInt(process.env.PORT || '3008', 10);
|
||||||
const DATA_DIR = process.env.DATA_DIR || "./data";
|
const DATA_DIR = process.env.DATA_DIR || './data';
|
||||||
const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== "false"; // Default to true
|
const ENABLE_REQUEST_LOGGING = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true
|
||||||
|
|
||||||
// Check for required environment variables
|
// Check for required environment variables
|
||||||
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
|
const hasAnthropicKey = !!process.env.ANTHROPIC_API_KEY;
|
||||||
@@ -71,7 +72,7 @@ if (!hasAnthropicKey) {
|
|||||||
╚═══════════════════════════════════════════════════════════════════════╝
|
╚═══════════════════════════════════════════════════════════════════════╝
|
||||||
`);
|
`);
|
||||||
} else {
|
} else {
|
||||||
console.log("[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)");
|
console.log('[Server] ✓ ANTHROPIC_API_KEY detected (API key auth)');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize security
|
// Initialize security
|
||||||
@@ -83,7 +84,7 @@ const app = express();
|
|||||||
// Middleware
|
// Middleware
|
||||||
// Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var)
|
// Custom colored logger showing only endpoint and status code (configurable via ENABLE_REQUEST_LOGGING env var)
|
||||||
if (ENABLE_REQUEST_LOGGING) {
|
if (ENABLE_REQUEST_LOGGING) {
|
||||||
morgan.token("status-colored", (req, res) => {
|
morgan.token('status-colored', (req, res) => {
|
||||||
const status = res.statusCode;
|
const status = res.statusCode;
|
||||||
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
|
if (status >= 500) return `\x1b[31m${status}\x1b[0m`; // Red for server errors
|
||||||
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
|
if (status >= 400) return `\x1b[33m${status}\x1b[0m`; // Yellow for client errors
|
||||||
@@ -92,18 +93,18 @@ if (ENABLE_REQUEST_LOGGING) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.use(
|
app.use(
|
||||||
morgan(":method :url :status-colored", {
|
morgan(':method :url :status-colored', {
|
||||||
skip: (req) => req.url === "/api/health", // Skip health check logs
|
skip: (req) => req.url === '/api/health', // Skip health check logs
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
origin: process.env.CORS_ORIGIN || "*",
|
origin: process.env.CORS_ORIGIN || '*',
|
||||||
credentials: true,
|
credentials: true,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
app.use(express.json({ limit: "50mb" }));
|
app.use(express.json({ limit: '50mb' }));
|
||||||
|
|
||||||
// Create shared event emitter for streaming
|
// Create shared event emitter for streaming
|
||||||
const events: EventEmitter = createEventEmitter();
|
const events: EventEmitter = createEventEmitter();
|
||||||
@@ -118,33 +119,34 @@ const claudeUsageService = new ClaudeUsageService();
|
|||||||
// Initialize services
|
// Initialize services
|
||||||
(async () => {
|
(async () => {
|
||||||
await agentService.initialize();
|
await agentService.initialize();
|
||||||
console.log("[Server] Agent service initialized");
|
console.log('[Server] Agent service initialized');
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Mount API routes - health is unauthenticated for monitoring
|
// Mount API routes - health is unauthenticated for monitoring
|
||||||
app.use("/api/health", createHealthRoutes());
|
app.use('/api/health', createHealthRoutes());
|
||||||
|
|
||||||
// Apply authentication to all other routes
|
// Apply authentication to all other routes
|
||||||
app.use("/api", authMiddleware);
|
app.use('/api', authMiddleware);
|
||||||
|
|
||||||
app.use("/api/fs", createFsRoutes(events));
|
app.use('/api/fs', createFsRoutes(events));
|
||||||
app.use("/api/agent", createAgentRoutes(agentService, events));
|
app.use('/api/agent', createAgentRoutes(agentService, events));
|
||||||
app.use("/api/sessions", createSessionsRoutes(agentService));
|
app.use('/api/sessions', createSessionsRoutes(agentService));
|
||||||
app.use("/api/features", createFeaturesRoutes(featureLoader));
|
app.use('/api/features', createFeaturesRoutes(featureLoader));
|
||||||
app.use("/api/auto-mode", createAutoModeRoutes(autoModeService));
|
app.use('/api/auto-mode', createAutoModeRoutes(autoModeService));
|
||||||
app.use("/api/enhance-prompt", createEnhancePromptRoutes());
|
app.use('/api/enhance-prompt', createEnhancePromptRoutes());
|
||||||
app.use("/api/worktree", createWorktreeRoutes());
|
app.use('/api/worktree', createWorktreeRoutes());
|
||||||
app.use("/api/git", createGitRoutes());
|
app.use('/api/git', createGitRoutes());
|
||||||
app.use("/api/setup", createSetupRoutes());
|
app.use('/api/setup', createSetupRoutes());
|
||||||
app.use("/api/suggestions", createSuggestionsRoutes(events));
|
app.use('/api/suggestions', createSuggestionsRoutes(events));
|
||||||
app.use("/api/models", createModelsRoutes());
|
app.use('/api/models', createModelsRoutes());
|
||||||
app.use("/api/spec-regeneration", createSpecRegenerationRoutes(events));
|
app.use('/api/spec-regeneration', createSpecRegenerationRoutes(events));
|
||||||
app.use("/api/running-agents", createRunningAgentsRoutes(autoModeService));
|
app.use('/api/running-agents', createRunningAgentsRoutes(autoModeService));
|
||||||
app.use("/api/workspace", createWorkspaceRoutes());
|
app.use('/api/workspace', createWorkspaceRoutes());
|
||||||
app.use("/api/templates", createTemplatesRoutes());
|
app.use('/api/templates', createTemplatesRoutes());
|
||||||
app.use("/api/terminal", createTerminalRoutes());
|
app.use('/api/terminal', createTerminalRoutes());
|
||||||
app.use("/api/settings", createSettingsRoutes(settingsService));
|
app.use('/api/settings', createSettingsRoutes(settingsService));
|
||||||
app.use("/api/claude", createClaudeRoutes(claudeUsageService));
|
app.use('/api/claude', createClaudeRoutes(claudeUsageService));
|
||||||
|
app.use('/api/context', createContextRoutes());
|
||||||
|
|
||||||
// Create HTTP server
|
// Create HTTP server
|
||||||
const server = createServer(app);
|
const server = createServer(app);
|
||||||
@@ -155,19 +157,16 @@ const terminalWss = new WebSocketServer({ noServer: true });
|
|||||||
const terminalService = getTerminalService();
|
const terminalService = getTerminalService();
|
||||||
|
|
||||||
// Handle HTTP upgrade requests manually to route to correct WebSocket server
|
// Handle HTTP upgrade requests manually to route to correct WebSocket server
|
||||||
server.on("upgrade", (request, socket, head) => {
|
server.on('upgrade', (request, socket, head) => {
|
||||||
const { pathname } = new URL(
|
const { pathname } = new URL(request.url || '', `http://${request.headers.host}`);
|
||||||
request.url || "",
|
|
||||||
`http://${request.headers.host}`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pathname === "/api/events") {
|
if (pathname === '/api/events') {
|
||||||
wss.handleUpgrade(request, socket, head, (ws) => {
|
wss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
wss.emit("connection", ws, request);
|
wss.emit('connection', ws, request);
|
||||||
});
|
});
|
||||||
} else if (pathname === "/api/terminal/ws") {
|
} else if (pathname === '/api/terminal/ws') {
|
||||||
terminalWss.handleUpgrade(request, socket, head, (ws) => {
|
terminalWss.handleUpgrade(request, socket, head, (ws) => {
|
||||||
terminalWss.emit("connection", ws, request);
|
terminalWss.emit('connection', ws, request);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
socket.destroy();
|
socket.destroy();
|
||||||
@@ -175,8 +174,8 @@ server.on("upgrade", (request, socket, head) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Events WebSocket connection handler
|
// Events WebSocket connection handler
|
||||||
wss.on("connection", (ws: WebSocket) => {
|
wss.on('connection', (ws: WebSocket) => {
|
||||||
console.log("[WebSocket] Client connected");
|
console.log('[WebSocket] Client connected');
|
||||||
|
|
||||||
// Subscribe to all events and forward to this client
|
// Subscribe to all events and forward to this client
|
||||||
const unsubscribe = events.subscribe((type, payload) => {
|
const unsubscribe = events.subscribe((type, payload) => {
|
||||||
@@ -185,13 +184,13 @@ wss.on("connection", (ws: WebSocket) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("close", () => {
|
ws.on('close', () => {
|
||||||
console.log("[WebSocket] Client disconnected");
|
console.log('[WebSocket] Client disconnected');
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("error", (error) => {
|
ws.on('error', (error) => {
|
||||||
console.error("[WebSocket] Error:", error);
|
console.error('[WebSocket] Error:', error);
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -212,184 +211,199 @@ terminalService.onExit((sessionId) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Terminal WebSocket connection handler
|
// Terminal WebSocket connection handler
|
||||||
terminalWss.on(
|
terminalWss.on('connection', (ws: WebSocket, req: import('http').IncomingMessage) => {
|
||||||
"connection",
|
// Parse URL to get session ID and token
|
||||||
(ws: WebSocket, req: import("http").IncomingMessage) => {
|
const url = new URL(req.url || '', `http://${req.headers.host}`);
|
||||||
// Parse URL to get session ID and token
|
const sessionId = url.searchParams.get('sessionId');
|
||||||
const url = new URL(req.url || "", `http://${req.headers.host}`);
|
const token = url.searchParams.get('token');
|
||||||
const sessionId = url.searchParams.get("sessionId");
|
|
||||||
const token = url.searchParams.get("token");
|
|
||||||
|
|
||||||
console.log(`[Terminal WS] Connection attempt for session: ${sessionId}`);
|
console.log(`[Terminal WS] Connection attempt for session: ${sessionId}`);
|
||||||
|
|
||||||
// Check if terminal is enabled
|
// Check if terminal is enabled
|
||||||
if (!isTerminalEnabled()) {
|
if (!isTerminalEnabled()) {
|
||||||
console.log("[Terminal WS] Terminal is disabled");
|
console.log('[Terminal WS] Terminal is disabled');
|
||||||
ws.close(4003, "Terminal access is disabled");
|
ws.close(4003, 'Terminal access is disabled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate token if password is required
|
// Validate token if password is required
|
||||||
if (
|
if (isTerminalPasswordRequired() && !validateTerminalToken(token || undefined)) {
|
||||||
isTerminalPasswordRequired() &&
|
console.log('[Terminal WS] Invalid or missing token');
|
||||||
!validateTerminalToken(token || undefined)
|
ws.close(4001, 'Authentication required');
|
||||||
) {
|
return;
|
||||||
console.log("[Terminal WS] Invalid or missing token");
|
}
|
||||||
ws.close(4001, "Authentication required");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
console.log("[Terminal WS] No session ID provided");
|
console.log('[Terminal WS] No session ID provided');
|
||||||
ws.close(4002, "Session ID required");
|
ws.close(4002, 'Session ID required');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if session exists
|
// Check if session exists
|
||||||
const session = terminalService.getSession(sessionId);
|
const session = terminalService.getSession(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
console.log(`[Terminal WS] Session ${sessionId} not found`);
|
console.log(`[Terminal WS] Session ${sessionId} not found`);
|
||||||
ws.close(4004, "Session not found");
|
ws.close(4004, 'Session not found');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[Terminal WS] Client connected to session ${sessionId}`);
|
console.log(`[Terminal WS] Client connected to session ${sessionId}`);
|
||||||
|
|
||||||
// Track this connection
|
// Track this connection
|
||||||
if (!terminalConnections.has(sessionId)) {
|
if (!terminalConnections.has(sessionId)) {
|
||||||
terminalConnections.set(sessionId, new Set());
|
terminalConnections.set(sessionId, new Set());
|
||||||
}
|
}
|
||||||
terminalConnections.get(sessionId)!.add(ws);
|
terminalConnections.get(sessionId)!.add(ws);
|
||||||
|
|
||||||
// Send initial connection success FIRST
|
// Send initial connection success FIRST
|
||||||
|
ws.send(
|
||||||
|
JSON.stringify({
|
||||||
|
type: 'connected',
|
||||||
|
sessionId,
|
||||||
|
shell: session.shell,
|
||||||
|
cwd: session.cwd,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Send scrollback buffer BEFORE subscribing to prevent race condition
|
||||||
|
// Also clear pending output buffer to prevent duplicates from throttled flush
|
||||||
|
const scrollback = terminalService.getScrollbackAndClearPending(sessionId);
|
||||||
|
if (scrollback && scrollback.length > 0) {
|
||||||
ws.send(
|
ws.send(
|
||||||
JSON.stringify({
|
JSON.stringify({
|
||||||
type: "connected",
|
type: 'scrollback',
|
||||||
sessionId,
|
data: scrollback,
|
||||||
shell: session.shell,
|
|
||||||
cwd: session.cwd,
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Send scrollback buffer BEFORE subscribing to prevent race condition
|
|
||||||
// Also clear pending output buffer to prevent duplicates from throttled flush
|
|
||||||
const scrollback = terminalService.getScrollbackAndClearPending(sessionId);
|
|
||||||
if (scrollback && scrollback.length > 0) {
|
|
||||||
ws.send(
|
|
||||||
JSON.stringify({
|
|
||||||
type: "scrollback",
|
|
||||||
data: scrollback,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOW subscribe to terminal data (after scrollback is sent)
|
|
||||||
const unsubscribeData = terminalService.onData((sid, data) => {
|
|
||||||
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(JSON.stringify({ type: "data", data }));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Subscribe to terminal exit
|
|
||||||
const unsubscribeExit = terminalService.onExit((sid, exitCode) => {
|
|
||||||
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
|
||||||
ws.send(JSON.stringify({ type: "exit", exitCode }));
|
|
||||||
ws.close(1000, "Session ended");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Handle incoming messages
|
|
||||||
ws.on("message", (message) => {
|
|
||||||
try {
|
|
||||||
const msg = JSON.parse(message.toString());
|
|
||||||
|
|
||||||
switch (msg.type) {
|
|
||||||
case "input":
|
|
||||||
// Write user input to terminal
|
|
||||||
terminalService.write(sessionId, msg.data);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "resize":
|
|
||||||
// Resize terminal with deduplication and rate limiting
|
|
||||||
if (msg.cols && msg.rows) {
|
|
||||||
const now = Date.now();
|
|
||||||
const lastTime = lastResizeTime.get(sessionId) || 0;
|
|
||||||
const lastDimensions = lastResizeDimensions.get(sessionId);
|
|
||||||
|
|
||||||
// Skip if resized too recently (prevents resize storm during splits)
|
|
||||||
if (now - lastTime < RESIZE_MIN_INTERVAL_MS) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if dimensions are different from last resize
|
|
||||||
if (
|
|
||||||
!lastDimensions ||
|
|
||||||
lastDimensions.cols !== msg.cols ||
|
|
||||||
lastDimensions.rows !== msg.rows
|
|
||||||
) {
|
|
||||||
// Only suppress output on subsequent resizes, not the first one
|
|
||||||
// The first resize happens on terminal open and we don't want to drop the initial prompt
|
|
||||||
const isFirstResize = !lastDimensions;
|
|
||||||
terminalService.resize(sessionId, msg.cols, msg.rows, !isFirstResize);
|
|
||||||
lastResizeDimensions.set(sessionId, {
|
|
||||||
cols: msg.cols,
|
|
||||||
rows: msg.rows,
|
|
||||||
});
|
|
||||||
lastResizeTime.set(sessionId, now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "ping":
|
|
||||||
// Respond to ping
|
|
||||||
ws.send(JSON.stringify({ type: "pong" }));
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.warn(`[Terminal WS] Unknown message type: ${msg.type}`);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("[Terminal WS] Error processing message:", error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("close", () => {
|
|
||||||
console.log(
|
|
||||||
`[Terminal WS] Client disconnected from session ${sessionId}`
|
|
||||||
);
|
|
||||||
unsubscribeData();
|
|
||||||
unsubscribeExit();
|
|
||||||
|
|
||||||
// Remove from connections tracking
|
|
||||||
const connections = terminalConnections.get(sessionId);
|
|
||||||
if (connections) {
|
|
||||||
connections.delete(ws);
|
|
||||||
if (connections.size === 0) {
|
|
||||||
terminalConnections.delete(sessionId);
|
|
||||||
// DON'T delete lastResizeDimensions/lastResizeTime here!
|
|
||||||
// The session still exists, and reconnecting clients need to know
|
|
||||||
// this isn't the "first resize" to prevent duplicate prompts.
|
|
||||||
// These get cleaned up when the session actually exits.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.on("error", (error) => {
|
|
||||||
console.error(`[Terminal WS] Error on session ${sessionId}:`, error);
|
|
||||||
unsubscribeData();
|
|
||||||
unsubscribeExit();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
// NOW subscribe to terminal data (after scrollback is sent)
|
||||||
|
const unsubscribeData = terminalService.onData((sid, data) => {
|
||||||
|
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'data', data }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to terminal exit
|
||||||
|
const unsubscribeExit = terminalService.onExit((sid, exitCode) => {
|
||||||
|
if (sid === sessionId && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({ type: 'exit', exitCode }));
|
||||||
|
ws.close(1000, 'Session ended');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle incoming messages
|
||||||
|
ws.on('message', (message) => {
|
||||||
|
try {
|
||||||
|
const msg = JSON.parse(message.toString());
|
||||||
|
|
||||||
|
switch (msg.type) {
|
||||||
|
case 'input':
|
||||||
|
// Validate input data type and length
|
||||||
|
if (typeof msg.data !== 'string') {
|
||||||
|
ws.send(JSON.stringify({ type: 'error', message: 'Invalid input type' }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Limit input size to 1MB to prevent memory issues
|
||||||
|
if (msg.data.length > 1024 * 1024) {
|
||||||
|
ws.send(JSON.stringify({ type: 'error', message: 'Input too large' }));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Write user input to terminal
|
||||||
|
terminalService.write(sessionId, msg.data);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'resize':
|
||||||
|
// Validate resize dimensions are positive integers within reasonable bounds
|
||||||
|
if (
|
||||||
|
typeof msg.cols !== 'number' ||
|
||||||
|
typeof msg.rows !== 'number' ||
|
||||||
|
!Number.isInteger(msg.cols) ||
|
||||||
|
!Number.isInteger(msg.rows) ||
|
||||||
|
msg.cols < 1 ||
|
||||||
|
msg.cols > 1000 ||
|
||||||
|
msg.rows < 1 ||
|
||||||
|
msg.rows > 500
|
||||||
|
) {
|
||||||
|
break; // Silently ignore invalid resize requests
|
||||||
|
}
|
||||||
|
// Resize terminal with deduplication and rate limiting
|
||||||
|
if (msg.cols && msg.rows) {
|
||||||
|
const now = Date.now();
|
||||||
|
const lastTime = lastResizeTime.get(sessionId) || 0;
|
||||||
|
const lastDimensions = lastResizeDimensions.get(sessionId);
|
||||||
|
|
||||||
|
// Skip if resized too recently (prevents resize storm during splits)
|
||||||
|
if (now - lastTime < RESIZE_MIN_INTERVAL_MS) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if dimensions are different from last resize
|
||||||
|
if (
|
||||||
|
!lastDimensions ||
|
||||||
|
lastDimensions.cols !== msg.cols ||
|
||||||
|
lastDimensions.rows !== msg.rows
|
||||||
|
) {
|
||||||
|
// Only suppress output on subsequent resizes, not the first one
|
||||||
|
// The first resize happens on terminal open and we don't want to drop the initial prompt
|
||||||
|
const isFirstResize = !lastDimensions;
|
||||||
|
terminalService.resize(sessionId, msg.cols, msg.rows, !isFirstResize);
|
||||||
|
lastResizeDimensions.set(sessionId, {
|
||||||
|
cols: msg.cols,
|
||||||
|
rows: msg.rows,
|
||||||
|
});
|
||||||
|
lastResizeTime.set(sessionId, now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'ping':
|
||||||
|
// Respond to ping
|
||||||
|
ws.send(JSON.stringify({ type: 'pong' }));
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`[Terminal WS] Unknown message type: ${msg.type}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Terminal WS] Error processing message:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('close', () => {
|
||||||
|
console.log(`[Terminal WS] Client disconnected from session ${sessionId}`);
|
||||||
|
unsubscribeData();
|
||||||
|
unsubscribeExit();
|
||||||
|
|
||||||
|
// Remove from connections tracking
|
||||||
|
const connections = terminalConnections.get(sessionId);
|
||||||
|
if (connections) {
|
||||||
|
connections.delete(ws);
|
||||||
|
if (connections.size === 0) {
|
||||||
|
terminalConnections.delete(sessionId);
|
||||||
|
// DON'T delete lastResizeDimensions/lastResizeTime here!
|
||||||
|
// The session still exists, and reconnecting clients need to know
|
||||||
|
// this isn't the "first resize" to prevent duplicate prompts.
|
||||||
|
// These get cleaned up when the session actually exits.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.on('error', (error) => {
|
||||||
|
console.error(`[Terminal WS] Error on session ${sessionId}:`, error);
|
||||||
|
unsubscribeData();
|
||||||
|
unsubscribeExit();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Start server with error handling for port conflicts
|
// Start server with error handling for port conflicts
|
||||||
const startServer = (port: number) => {
|
const startServer = (port: number) => {
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
const terminalStatus = isTerminalEnabled()
|
const terminalStatus = isTerminalEnabled()
|
||||||
? isTerminalPasswordRequired()
|
? isTerminalPasswordRequired()
|
||||||
? "enabled (password protected)"
|
? 'enabled (password protected)'
|
||||||
: "enabled"
|
: 'enabled'
|
||||||
: "disabled";
|
: 'disabled';
|
||||||
const portStr = port.toString().padEnd(4);
|
const portStr = port.toString().padEnd(4);
|
||||||
console.log(`
|
console.log(`
|
||||||
╔═══════════════════════════════════════════════════════╗
|
╔═══════════════════════════════════════════════════════╗
|
||||||
@@ -404,8 +418,8 @@ const startServer = (port: number) => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
server.on("error", (error: NodeJS.ErrnoException) => {
|
server.on('error', (error: NodeJS.ErrnoException) => {
|
||||||
if (error.code === "EADDRINUSE") {
|
if (error.code === 'EADDRINUSE') {
|
||||||
console.error(`
|
console.error(`
|
||||||
╔═══════════════════════════════════════════════════════╗
|
╔═══════════════════════════════════════════════════════╗
|
||||||
║ ❌ ERROR: Port ${port} is already in use ║
|
║ ❌ ERROR: Port ${port} is already in use ║
|
||||||
@@ -426,7 +440,7 @@ const startServer = (port: number) => {
|
|||||||
`);
|
`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
} else {
|
} else {
|
||||||
console.error("[Server] Error starting server:", error);
|
console.error('[Server] Error starting server:', error);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -435,20 +449,20 @@ const startServer = (port: number) => {
|
|||||||
startServer(PORT);
|
startServer(PORT);
|
||||||
|
|
||||||
// Graceful shutdown
|
// Graceful shutdown
|
||||||
process.on("SIGTERM", () => {
|
process.on('SIGTERM', () => {
|
||||||
console.log("SIGTERM received, shutting down...");
|
console.log('SIGTERM received, shutting down...');
|
||||||
terminalService.cleanup();
|
terminalService.cleanup();
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
console.log("Server closed");
|
console.log('Server closed');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on('SIGINT', () => {
|
||||||
console.log("SIGINT received, shutting down...");
|
console.log('SIGINT received, shutting down...');
|
||||||
terminalService.cleanup();
|
terminalService.cleanup();
|
||||||
server.close(() => {
|
server.close(() => {
|
||||||
console.log("Server closed");
|
console.log('Server closed');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,26 +6,26 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Import and re-export spec types from shared package
|
// Import and re-export spec types from shared package
|
||||||
export type { SpecOutput } from "@automaker/types";
|
export type { SpecOutput } from '@automaker/types';
|
||||||
export { specOutputSchema } from "@automaker/types";
|
export { specOutputSchema } from '@automaker/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Escape special XML characters
|
* Escape special XML characters
|
||||||
*/
|
*/
|
||||||
function escapeXml(str: string): string {
|
function escapeXml(str: string): string {
|
||||||
return str
|
return str
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, '&')
|
||||||
.replace(/</g, "<")
|
.replace(/</g, '<')
|
||||||
.replace(/>/g, ">")
|
.replace(/>/g, '>')
|
||||||
.replace(/"/g, """)
|
.replace(/"/g, '"')
|
||||||
.replace(/'/g, "'");
|
.replace(/'/g, ''');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert structured spec output to XML format
|
* Convert structured spec output to XML format
|
||||||
*/
|
*/
|
||||||
export function specToXml(spec: import("@automaker/types").SpecOutput): string {
|
export function specToXml(spec: import('@automaker/types').SpecOutput): string {
|
||||||
const indent = " ";
|
const indent = ' ';
|
||||||
|
|
||||||
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project_specification>
|
<project_specification>
|
||||||
@@ -36,11 +36,11 @@ ${indent}${indent}${escapeXml(spec.overview)}
|
|||||||
${indent}</overview>
|
${indent}</overview>
|
||||||
|
|
||||||
${indent}<technology_stack>
|
${indent}<technology_stack>
|
||||||
${spec.technology_stack.map((t) => `${indent}${indent}<technology>${escapeXml(t)}</technology>`).join("\n")}
|
${spec.technology_stack.map((t) => `${indent}${indent}<technology>${escapeXml(t)}</technology>`).join('\n')}
|
||||||
${indent}</technology_stack>
|
${indent}</technology_stack>
|
||||||
|
|
||||||
${indent}<core_capabilities>
|
${indent}<core_capabilities>
|
||||||
${spec.core_capabilities.map((c) => `${indent}${indent}<capability>${escapeXml(c)}</capability>`).join("\n")}
|
${spec.core_capabilities.map((c) => `${indent}${indent}<capability>${escapeXml(c)}</capability>`).join('\n')}
|
||||||
${indent}</core_capabilities>
|
${indent}</core_capabilities>
|
||||||
|
|
||||||
${indent}<implemented_features>
|
${indent}<implemented_features>
|
||||||
@@ -51,13 +51,13 @@ ${indent}${indent}${indent}<name>${escapeXml(f.name)}</name>
|
|||||||
${indent}${indent}${indent}<description>${escapeXml(f.description)}</description>${
|
${indent}${indent}${indent}<description>${escapeXml(f.description)}</description>${
|
||||||
f.file_locations && f.file_locations.length > 0
|
f.file_locations && f.file_locations.length > 0
|
||||||
? `\n${indent}${indent}${indent}<file_locations>
|
? `\n${indent}${indent}${indent}<file_locations>
|
||||||
${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}<location>${escapeXml(loc)}</location>`).join("\n")}
|
${f.file_locations.map((loc) => `${indent}${indent}${indent}${indent}<location>${escapeXml(loc)}</location>`).join('\n')}
|
||||||
${indent}${indent}${indent}</file_locations>`
|
${indent}${indent}${indent}</file_locations>`
|
||||||
: ""
|
: ''
|
||||||
}
|
}
|
||||||
${indent}${indent}</feature>`
|
${indent}${indent}</feature>`
|
||||||
)
|
)
|
||||||
.join("\n")}
|
.join('\n')}
|
||||||
${indent}</implemented_features>`;
|
${indent}</implemented_features>`;
|
||||||
|
|
||||||
// Optional sections
|
// Optional sections
|
||||||
@@ -65,7 +65,7 @@ ${indent}</implemented_features>`;
|
|||||||
xml += `
|
xml += `
|
||||||
|
|
||||||
${indent}<additional_requirements>
|
${indent}<additional_requirements>
|
||||||
${spec.additional_requirements.map((r) => `${indent}${indent}<requirement>${escapeXml(r)}</requirement>`).join("\n")}
|
${spec.additional_requirements.map((r) => `${indent}${indent}<requirement>${escapeXml(r)}</requirement>`).join('\n')}
|
||||||
${indent}</additional_requirements>`;
|
${indent}</additional_requirements>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ ${indent}</additional_requirements>`;
|
|||||||
xml += `
|
xml += `
|
||||||
|
|
||||||
${indent}<development_guidelines>
|
${indent}<development_guidelines>
|
||||||
${spec.development_guidelines.map((g) => `${indent}${indent}<guideline>${escapeXml(g)}</guideline>`).join("\n")}
|
${spec.development_guidelines.map((g) => `${indent}${indent}<guideline>${escapeXml(g)}</guideline>`).join('\n')}
|
||||||
${indent}</development_guidelines>`;
|
${indent}</development_guidelines>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ ${indent}${indent}${indent}<status>${escapeXml(r.status)}</status>
|
|||||||
${indent}${indent}${indent}<description>${escapeXml(r.description)}</description>
|
${indent}${indent}${indent}<description>${escapeXml(r.description)}</description>
|
||||||
${indent}${indent}</phase>`
|
${indent}${indent}</phase>`
|
||||||
)
|
)
|
||||||
.join("\n")}
|
.join('\n')}
|
||||||
${indent}</implementation_roadmap>`;
|
${indent}</implementation_roadmap>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Supports API key authentication via header or environment variable.
|
* Supports API key authentication via header or environment variable.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response, NextFunction } from "express";
|
import type { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
// API key from environment (optional - if not set, auth is disabled)
|
// API key from environment (optional - if not set, auth is disabled)
|
||||||
const API_KEY = process.env.AUTOMAKER_API_KEY;
|
const API_KEY = process.env.AUTOMAKER_API_KEY;
|
||||||
@@ -23,12 +23,12 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for API key in header
|
// Check for API key in header
|
||||||
const providedKey = req.headers["x-api-key"] as string | undefined;
|
const providedKey = req.headers['x-api-key'] as string | undefined;
|
||||||
|
|
||||||
if (!providedKey) {
|
if (!providedKey) {
|
||||||
res.status(401).json({
|
res.status(401).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Authentication required. Provide X-API-Key header.",
|
error: 'Authentication required. Provide X-API-Key header.',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
|
|||||||
if (providedKey !== API_KEY) {
|
if (providedKey !== API_KEY) {
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid API key.",
|
error: 'Invalid API key.',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -57,6 +57,6 @@ export function isAuthEnabled(): boolean {
|
|||||||
export function getAuthStatus(): { enabled: boolean; method: string } {
|
export function getAuthStatus(): { enabled: boolean; method: string } {
|
||||||
return {
|
return {
|
||||||
enabled: !!API_KEY,
|
enabled: !!API_KEY,
|
||||||
method: API_KEY ? "api_key" : "none",
|
method: API_KEY ? 'api_key' : 'none',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Event emitter for streaming events to WebSocket clients
|
* Event emitter for streaming events to WebSocket clients
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { EventType, EventCallback } from "@automaker/types";
|
import type { EventType, EventCallback } from '@automaker/types';
|
||||||
|
|
||||||
// Re-export event types from shared package
|
// Re-export event types from shared package
|
||||||
export type { EventType, EventCallback };
|
export type { EventType, EventCallback };
|
||||||
@@ -21,7 +21,7 @@ export function createEventEmitter(): EventEmitter {
|
|||||||
try {
|
try {
|
||||||
callback(type, payload);
|
callback(type, payload);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error in event subscriber:", error);
|
console.error('Error in event subscriber:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* This file exists for backward compatibility with existing imports
|
* This file exists for backward compatibility with existing imports
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { secureFs } from "@automaker/platform";
|
import { secureFs } from '@automaker/platform';
|
||||||
|
|
||||||
export const {
|
export const {
|
||||||
access,
|
access,
|
||||||
|
|||||||
@@ -4,8 +4,8 @@
|
|||||||
* try-catch block in every route handler
|
* try-catch block in every route handler
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response, NextFunction } from "express";
|
import type { Request, Response, NextFunction } from 'express';
|
||||||
import { validatePath, PathNotAllowedError } from "@automaker/platform";
|
import { validatePath, PathNotAllowedError } from '@automaker/platform';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a middleware that validates specified path parameters in req.body
|
* Creates a middleware that validates specified path parameters in req.body
|
||||||
@@ -24,7 +24,7 @@ export function validatePathParams(...paramNames: string[]) {
|
|||||||
try {
|
try {
|
||||||
for (const paramName of paramNames) {
|
for (const paramName of paramNames) {
|
||||||
// Handle optional parameters (paramName?)
|
// Handle optional parameters (paramName?)
|
||||||
if (paramName.endsWith("?")) {
|
if (paramName.endsWith('?')) {
|
||||||
const actualName = paramName.slice(0, -1);
|
const actualName = paramName.slice(0, -1);
|
||||||
const value = req.body[actualName];
|
const value = req.body[actualName];
|
||||||
if (value) {
|
if (value) {
|
||||||
@@ -34,7 +34,7 @@ export function validatePathParams(...paramNames: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle array parameters (paramName[])
|
// Handle array parameters (paramName[])
|
||||||
if (paramName.endsWith("[]")) {
|
if (paramName.endsWith('[]')) {
|
||||||
const actualName = paramName.slice(0, -2);
|
const actualName = paramName.slice(0, -2);
|
||||||
const values = req.body[actualName];
|
const values = req.body[actualName];
|
||||||
if (Array.isArray(values) && values.length > 0) {
|
if (Array.isArray(values) && values.length > 0) {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type {
|
|||||||
InstallationStatus,
|
InstallationStatus,
|
||||||
ValidationResult,
|
ValidationResult,
|
||||||
ModelDefinition,
|
ModelDefinition,
|
||||||
} from "./types.js";
|
} from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base provider class that all provider implementations must extend
|
* Base provider class that all provider implementations must extend
|
||||||
@@ -33,9 +33,7 @@ export abstract class BaseProvider {
|
|||||||
* @param options Execution options
|
* @param options Execution options
|
||||||
* @returns AsyncGenerator yielding provider messages
|
* @returns AsyncGenerator yielding provider messages
|
||||||
*/
|
*/
|
||||||
abstract executeQuery(
|
abstract executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage>;
|
||||||
options: ExecuteOptions
|
|
||||||
): AsyncGenerator<ProviderMessage>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect if the provider is installed and configured
|
* Detect if the provider is installed and configured
|
||||||
@@ -59,7 +57,7 @@ export abstract class BaseProvider {
|
|||||||
|
|
||||||
// Base validation (can be overridden)
|
// Base validation (can be overridden)
|
||||||
if (!this.config) {
|
if (!this.config) {
|
||||||
errors.push("Provider config is missing");
|
errors.push('Provider config is missing');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -76,7 +74,7 @@ export abstract class BaseProvider {
|
|||||||
*/
|
*/
|
||||||
supportsFeature(feature: string): boolean {
|
supportsFeature(feature: string): boolean {
|
||||||
// Default implementation - override in subclasses
|
// Default implementation - override in subclasses
|
||||||
const commonFeatures = ["tools", "text"];
|
const commonFeatures = ['tools', 'text'];
|
||||||
return commonFeatures.includes(feature);
|
return commonFeatures.includes(feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,26 +5,24 @@
|
|||||||
* with the provider architecture.
|
* with the provider architecture.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { BaseProvider } from "./base-provider.js";
|
import { BaseProvider } from './base-provider.js';
|
||||||
import type {
|
import type {
|
||||||
ExecuteOptions,
|
ExecuteOptions,
|
||||||
ProviderMessage,
|
ProviderMessage,
|
||||||
InstallationStatus,
|
InstallationStatus,
|
||||||
ModelDefinition,
|
ModelDefinition,
|
||||||
} from "./types.js";
|
} from './types.js';
|
||||||
|
|
||||||
export class ClaudeProvider extends BaseProvider {
|
export class ClaudeProvider extends BaseProvider {
|
||||||
getName(): string {
|
getName(): string {
|
||||||
return "claude";
|
return 'claude';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute a query using Claude Agent SDK
|
* Execute a query using Claude Agent SDK
|
||||||
*/
|
*/
|
||||||
async *executeQuery(
|
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
||||||
options: ExecuteOptions
|
|
||||||
): AsyncGenerator<ProviderMessage> {
|
|
||||||
const {
|
const {
|
||||||
prompt,
|
prompt,
|
||||||
model,
|
model,
|
||||||
@@ -38,16 +36,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
// Build Claude SDK options
|
// Build Claude SDK options
|
||||||
const defaultTools = [
|
const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
];
|
|
||||||
const toolsToUse = allowedTools || defaultTools;
|
const toolsToUse = allowedTools || defaultTools;
|
||||||
|
|
||||||
const sdkOptions: Options = {
|
const sdkOptions: Options = {
|
||||||
@@ -56,7 +45,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
maxTurns,
|
maxTurns,
|
||||||
cwd,
|
cwd,
|
||||||
allowedTools: toolsToUse,
|
allowedTools: toolsToUse,
|
||||||
permissionMode: "acceptEdits",
|
permissionMode: 'acceptEdits',
|
||||||
sandbox: {
|
sandbox: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
autoAllowBashIfSandboxed: true,
|
autoAllowBashIfSandboxed: true,
|
||||||
@@ -75,10 +64,10 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
// Multi-part prompt (with images)
|
// Multi-part prompt (with images)
|
||||||
promptPayload = (async function* () {
|
promptPayload = (async function* () {
|
||||||
const multiPartPrompt = {
|
const multiPartPrompt = {
|
||||||
type: "user" as const,
|
type: 'user' as const,
|
||||||
session_id: "",
|
session_id: '',
|
||||||
message: {
|
message: {
|
||||||
role: "user" as const,
|
role: 'user' as const,
|
||||||
content: prompt,
|
content: prompt,
|
||||||
},
|
},
|
||||||
parent_tool_use_id: null,
|
parent_tool_use_id: null,
|
||||||
@@ -99,10 +88,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
yield msg as ProviderMessage;
|
yield msg as ProviderMessage;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error('[ClaudeProvider] executeQuery() error during execution:', error);
|
||||||
"[ClaudeProvider] executeQuery() error during execution:",
|
|
||||||
error
|
|
||||||
);
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +102,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
|
|
||||||
const status: InstallationStatus = {
|
const status: InstallationStatus = {
|
||||||
installed: true,
|
installed: true,
|
||||||
method: "sdk",
|
method: 'sdk',
|
||||||
hasApiKey,
|
hasApiKey,
|
||||||
authenticated: hasApiKey,
|
authenticated: hasApiKey,
|
||||||
};
|
};
|
||||||
@@ -130,53 +116,53 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
getAvailableModels(): ModelDefinition[] {
|
getAvailableModels(): ModelDefinition[] {
|
||||||
const models = [
|
const models = [
|
||||||
{
|
{
|
||||||
id: "claude-opus-4-5-20251101",
|
id: 'claude-opus-4-5-20251101',
|
||||||
name: "Claude Opus 4.5",
|
name: 'Claude Opus 4.5',
|
||||||
modelString: "claude-opus-4-5-20251101",
|
modelString: 'claude-opus-4-5-20251101',
|
||||||
provider: "anthropic",
|
provider: 'anthropic',
|
||||||
description: "Most capable Claude model",
|
description: 'Most capable Claude model',
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxOutputTokens: 16000,
|
maxOutputTokens: 16000,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
tier: "premium" as const,
|
tier: 'premium' as const,
|
||||||
default: true,
|
default: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-20250514",
|
id: 'claude-sonnet-4-20250514',
|
||||||
name: "Claude Sonnet 4",
|
name: 'Claude Sonnet 4',
|
||||||
modelString: "claude-sonnet-4-20250514",
|
modelString: 'claude-sonnet-4-20250514',
|
||||||
provider: "anthropic",
|
provider: 'anthropic',
|
||||||
description: "Balanced performance and cost",
|
description: 'Balanced performance and cost',
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxOutputTokens: 16000,
|
maxOutputTokens: 16000,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
tier: "standard" as const,
|
tier: 'standard' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "claude-3-5-sonnet-20241022",
|
id: 'claude-3-5-sonnet-20241022',
|
||||||
name: "Claude 3.5 Sonnet",
|
name: 'Claude 3.5 Sonnet',
|
||||||
modelString: "claude-3-5-sonnet-20241022",
|
modelString: 'claude-3-5-sonnet-20241022',
|
||||||
provider: "anthropic",
|
provider: 'anthropic',
|
||||||
description: "Fast and capable",
|
description: 'Fast and capable',
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxOutputTokens: 8000,
|
maxOutputTokens: 8000,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
tier: "standard" as const,
|
tier: 'standard' as const,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "claude-3-5-haiku-20241022",
|
id: 'claude-haiku-4-5-20251001',
|
||||||
name: "Claude 3.5 Haiku",
|
name: 'Claude Haiku 4.5',
|
||||||
modelString: "claude-3-5-haiku-20241022",
|
modelString: 'claude-haiku-4-5-20251001',
|
||||||
provider: "anthropic",
|
provider: 'anthropic',
|
||||||
description: "Fastest Claude model",
|
description: 'Fastest Claude model',
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxOutputTokens: 8000,
|
maxOutputTokens: 8000,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
tier: "basic" as const,
|
tier: 'basic' as const,
|
||||||
},
|
},
|
||||||
] satisfies ModelDefinition[];
|
] satisfies ModelDefinition[];
|
||||||
return models;
|
return models;
|
||||||
@@ -186,7 +172,7 @@ export class ClaudeProvider extends BaseProvider {
|
|||||||
* Check if the provider supports a specific feature
|
* Check if the provider supports a specific feature
|
||||||
*/
|
*/
|
||||||
supportsFeature(feature: string): boolean {
|
supportsFeature(feature: string): boolean {
|
||||||
const supportedFeatures = ["tools", "text", "vision", "thinking"];
|
const supportedFeatures = ['tools', 'text', 'vision', 'thinking'];
|
||||||
return supportedFeatures.includes(feature);
|
return supportedFeatures.includes(feature);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
* new providers (Cursor, OpenCode, etc.) trivial - just add one line.
|
* new providers (Cursor, OpenCode, etc.) trivial - just add one line.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { BaseProvider } from "./base-provider.js";
|
import { BaseProvider } from './base-provider.js';
|
||||||
import { ClaudeProvider } from "./claude-provider.js";
|
import { ClaudeProvider } from './claude-provider.js';
|
||||||
import type { InstallationStatus } from "./types.js";
|
import type { InstallationStatus } from './types.js';
|
||||||
|
|
||||||
export class ProviderFactory {
|
export class ProviderFactory {
|
||||||
/**
|
/**
|
||||||
@@ -21,10 +21,7 @@ export class ProviderFactory {
|
|||||||
const lowerModel = modelId.toLowerCase();
|
const lowerModel = modelId.toLowerCase();
|
||||||
|
|
||||||
// Claude models (claude-*, opus, sonnet, haiku)
|
// Claude models (claude-*, opus, sonnet, haiku)
|
||||||
if (
|
if (lowerModel.startsWith('claude-') || ['haiku', 'sonnet', 'opus'].includes(lowerModel)) {
|
||||||
lowerModel.startsWith("claude-") ||
|
|
||||||
["haiku", "sonnet", "opus"].includes(lowerModel)
|
|
||||||
) {
|
|
||||||
return new ClaudeProvider();
|
return new ClaudeProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,9 +34,7 @@ export class ProviderFactory {
|
|||||||
// }
|
// }
|
||||||
|
|
||||||
// Default to Claude for unknown models
|
// Default to Claude for unknown models
|
||||||
console.warn(
|
console.warn(`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`);
|
||||||
`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`
|
|
||||||
);
|
|
||||||
return new ClaudeProvider();
|
return new ClaudeProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,9 +53,7 @@ export class ProviderFactory {
|
|||||||
*
|
*
|
||||||
* @returns Map of provider name to installation status
|
* @returns Map of provider name to installation status
|
||||||
*/
|
*/
|
||||||
static async checkAllProviders(): Promise<
|
static async checkAllProviders(): Promise<Record<string, InstallationStatus>> {
|
||||||
Record<string, InstallationStatus>
|
|
||||||
> {
|
|
||||||
const providers = this.getAllProviders();
|
const providers = this.getAllProviders();
|
||||||
const statuses: Record<string, InstallationStatus> = {};
|
const statuses: Record<string, InstallationStatus> = {};
|
||||||
|
|
||||||
@@ -83,8 +76,8 @@ export class ProviderFactory {
|
|||||||
const lowerName = name.toLowerCase();
|
const lowerName = name.toLowerCase();
|
||||||
|
|
||||||
switch (lowerName) {
|
switch (lowerName) {
|
||||||
case "claude":
|
case 'claude':
|
||||||
case "anthropic":
|
case 'anthropic':
|
||||||
return new ClaudeProvider();
|
return new ClaudeProvider();
|
||||||
|
|
||||||
// Future providers:
|
// Future providers:
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export interface ProviderConfig {
|
|||||||
* Message in conversation history
|
* Message in conversation history
|
||||||
*/
|
*/
|
||||||
export interface ConversationMessage {
|
export interface ConversationMessage {
|
||||||
role: "user" | "assistant";
|
role: 'user' | 'assistant';
|
||||||
content: string | Array<{ type: string; text?: string; source?: object }>;
|
content: string | Array<{ type: string; text?: string; source?: object }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ export interface ExecuteOptions {
|
|||||||
* Content block in a provider message (matches Claude SDK format)
|
* Content block in a provider message (matches Claude SDK format)
|
||||||
*/
|
*/
|
||||||
export interface ContentBlock {
|
export interface ContentBlock {
|
||||||
type: "text" | "tool_use" | "thinking" | "tool_result";
|
type: 'text' | 'tool_use' | 'thinking' | 'tool_result';
|
||||||
text?: string;
|
text?: string;
|
||||||
thinking?: string;
|
thinking?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -52,11 +52,11 @@ export interface ContentBlock {
|
|||||||
* Message returned by a provider (matches Claude SDK streaming format)
|
* Message returned by a provider (matches Claude SDK streaming format)
|
||||||
*/
|
*/
|
||||||
export interface ProviderMessage {
|
export interface ProviderMessage {
|
||||||
type: "assistant" | "user" | "error" | "result";
|
type: 'assistant' | 'user' | 'error' | 'result';
|
||||||
subtype?: "success" | "error";
|
subtype?: 'success' | 'error';
|
||||||
session_id?: string;
|
session_id?: string;
|
||||||
message?: {
|
message?: {
|
||||||
role: "user" | "assistant";
|
role: 'user' | 'assistant';
|
||||||
content: ContentBlock[];
|
content: ContentBlock[];
|
||||||
};
|
};
|
||||||
result?: string;
|
result?: string;
|
||||||
@@ -71,7 +71,7 @@ export interface InstallationStatus {
|
|||||||
installed: boolean;
|
installed: boolean;
|
||||||
path?: string;
|
path?: string;
|
||||||
version?: string;
|
version?: string;
|
||||||
method?: "cli" | "npm" | "brew" | "sdk";
|
method?: 'cli' | 'npm' | 'brew' | 'sdk';
|
||||||
hasApiKey?: boolean;
|
hasApiKey?: boolean;
|
||||||
authenticated?: boolean;
|
authenticated?: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -99,6 +99,6 @@ export interface ModelDefinition {
|
|||||||
maxOutputTokens?: number;
|
maxOutputTokens?: number;
|
||||||
supportsVision?: boolean;
|
supportsVision?: boolean;
|
||||||
supportsTools?: boolean;
|
supportsTools?: boolean;
|
||||||
tier?: "basic" | "standard" | "premium";
|
tier?: 'basic' | 'standard' | 'premium';
|
||||||
default?: boolean;
|
default?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
* Common utilities for agent routes
|
* Common utilities for agent routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("Agent");
|
const logger = createLogger('Agent');
|
||||||
|
|
||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -2,29 +2,30 @@
|
|||||||
* Agent routes - HTTP API for Claude agent interactions
|
* Agent routes - HTTP API for Claude agent interactions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import { AgentService } from "../../services/agent-service.js";
|
import { AgentService } from '../../services/agent-service.js';
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { validatePathParams } from "../../middleware/validate-paths.js";
|
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||||
import { createStartHandler } from "./routes/start.js";
|
import { createStartHandler } from './routes/start.js';
|
||||||
import { createSendHandler } from "./routes/send.js";
|
import { createSendHandler } from './routes/send.js';
|
||||||
import { createHistoryHandler } from "./routes/history.js";
|
import { createHistoryHandler } from './routes/history.js';
|
||||||
import { createStopHandler } from "./routes/stop.js";
|
import { createStopHandler } from './routes/stop.js';
|
||||||
import { createClearHandler } from "./routes/clear.js";
|
import { createClearHandler } from './routes/clear.js';
|
||||||
import { createModelHandler } from "./routes/model.js";
|
import { createModelHandler } from './routes/model.js';
|
||||||
|
|
||||||
export function createAgentRoutes(
|
export function createAgentRoutes(agentService: AgentService, _events: EventEmitter): Router {
|
||||||
agentService: AgentService,
|
|
||||||
_events: EventEmitter
|
|
||||||
): Router {
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/start", validatePathParams("workingDirectory?"), createStartHandler(agentService));
|
router.post('/start', validatePathParams('workingDirectory?'), createStartHandler(agentService));
|
||||||
router.post("/send", validatePathParams("workingDirectory?", "imagePaths[]"), createSendHandler(agentService));
|
router.post(
|
||||||
router.post("/history", createHistoryHandler(agentService));
|
'/send',
|
||||||
router.post("/stop", createStopHandler(agentService));
|
validatePathParams('workingDirectory?', 'imagePaths[]'),
|
||||||
router.post("/clear", createClearHandler(agentService));
|
createSendHandler(agentService)
|
||||||
router.post("/model", createModelHandler(agentService));
|
);
|
||||||
|
router.post('/history', createHistoryHandler(agentService));
|
||||||
|
router.post('/stop', createStopHandler(agentService));
|
||||||
|
router.post('/clear', createClearHandler(agentService));
|
||||||
|
router.post('/model', createModelHandler(agentService));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /clear endpoint - Clear conversation
|
* POST /clear endpoint - Clear conversation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createClearHandler(agentService: AgentService) {
|
export function createClearHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -12,16 +12,14 @@ export function createClearHandler(agentService: AgentService) {
|
|||||||
const { sessionId } = req.body as { sessionId: string };
|
const { sessionId } = req.body as { sessionId: string };
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "sessionId is required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await agentService.clearSession(sessionId);
|
const result = await agentService.clearSession(sessionId);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Clear session failed");
|
logError(error, 'Clear session failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /history endpoint - Get conversation history
|
* POST /history endpoint - Get conversation history
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createHistoryHandler(agentService: AgentService) {
|
export function createHistoryHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -12,16 +12,14 @@ export function createHistoryHandler(agentService: AgentService) {
|
|||||||
const { sessionId } = req.body as { sessionId: string };
|
const { sessionId } = req.body as { sessionId: string };
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "sessionId is required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = agentService.getHistory(sessionId);
|
const result = agentService.getHistory(sessionId);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get history failed");
|
logError(error, 'Get history failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /model endpoint - Set session model
|
* POST /model endpoint - Set session model
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createModelHandler(agentService: AgentService) {
|
export function createModelHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -15,16 +15,14 @@ export function createModelHandler(agentService: AgentService) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!sessionId || !model) {
|
if (!sessionId || !model) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'sessionId and model are required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "sessionId and model are required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await agentService.setSessionModel(sessionId, model);
|
const result = await agentService.setSessionModel(sessionId, model);
|
||||||
res.json({ success: result });
|
res.json({ success: result });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Set session model failed");
|
logError(error, 'Set session model failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,28 +2,27 @@
|
|||||||
* POST /send endpoint - Send a message
|
* POST /send endpoint - Send a message
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
const logger = createLogger("Agent");
|
const logger = createLogger('Agent');
|
||||||
|
|
||||||
export function createSendHandler(agentService: AgentService) {
|
export function createSendHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { sessionId, message, workingDirectory, imagePaths, model } =
|
const { sessionId, message, workingDirectory, imagePaths, model } = req.body as {
|
||||||
req.body as {
|
sessionId: string;
|
||||||
sessionId: string;
|
message: string;
|
||||||
message: string;
|
workingDirectory?: string;
|
||||||
workingDirectory?: string;
|
imagePaths?: string[];
|
||||||
imagePaths?: string[];
|
model?: string;
|
||||||
model?: string;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
if (!sessionId || !message) {
|
if (!sessionId || !message) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "sessionId and message are required",
|
error: 'sessionId and message are required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -38,13 +37,13 @@ export function createSendHandler(agentService: AgentService) {
|
|||||||
model,
|
model,
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logError(error, "Send message failed (background)");
|
logError(error, 'Send message failed (background)');
|
||||||
});
|
});
|
||||||
|
|
||||||
// Return immediately - responses come via WebSocket
|
// Return immediately - responses come via WebSocket
|
||||||
res.json({ success: true, message: "Message sent" });
|
res.json({ success: true, message: 'Message sent' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Send message failed");
|
logError(error, 'Send message failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* POST /start endpoint - Start a conversation
|
* POST /start endpoint - Start a conversation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
const logger = createLogger("Agent");
|
const logger = createLogger('Agent');
|
||||||
|
|
||||||
export function createStartHandler(agentService: AgentService) {
|
export function createStartHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -17,9 +17,7 @@ export function createStartHandler(agentService: AgentService) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "sessionId is required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +28,7 @@ export function createStartHandler(agentService: AgentService) {
|
|||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Start conversation failed");
|
logError(error, 'Start conversation failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /stop endpoint - Stop execution
|
* POST /stop endpoint - Stop execution
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createStopHandler(agentService: AgentService) {
|
export function createStopHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -12,16 +12,14 @@ export function createStopHandler(agentService: AgentService) {
|
|||||||
const { sessionId } = req.body as { sessionId: string };
|
const { sessionId } = req.body as { sessionId: string };
|
||||||
|
|
||||||
if (!sessionId) {
|
if (!sessionId) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "sessionId is required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await agentService.stopExecution(sessionId);
|
const result = await agentService.stopExecution(sessionId);
|
||||||
res.json(result);
|
res.json(result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Stop execution failed");
|
logError(error, 'Stop execution failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* Common utilities and state management for spec regeneration
|
* Common utilities and state management for spec regeneration
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
const logger = createLogger("SpecRegeneration");
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
// Shared state for tracking generation status - private
|
// Shared state for tracking generation status - private
|
||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
@@ -23,10 +23,7 @@ export function getSpecRegenerationStatus(): {
|
|||||||
/**
|
/**
|
||||||
* Set the running state and abort controller
|
* Set the running state and abort controller
|
||||||
*/
|
*/
|
||||||
export function setRunningState(
|
export function setRunningState(running: boolean, controller: AbortController | null = null): void {
|
||||||
running: boolean,
|
|
||||||
controller: AbortController | null = null
|
|
||||||
): void {
|
|
||||||
isRunning = running;
|
isRunning = running;
|
||||||
currentAbortController = controller;
|
currentAbortController = controller;
|
||||||
}
|
}
|
||||||
@@ -40,14 +37,12 @@ export function logAuthStatus(context: string): void {
|
|||||||
logger.info(`${context} - Auth Status:`);
|
logger.info(`${context} - Auth Status:`);
|
||||||
logger.info(
|
logger.info(
|
||||||
` ANTHROPIC_API_KEY: ${
|
` ANTHROPIC_API_KEY: ${
|
||||||
hasApiKey
|
hasApiKey ? 'SET (' + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + '...)' : 'NOT SET'
|
||||||
? "SET (" + process.env.ANTHROPIC_API_KEY?.substring(0, 20) + "...)"
|
|
||||||
: "NOT SET"
|
|
||||||
}`
|
}`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!hasApiKey) {
|
if (!hasApiKey) {
|
||||||
logger.warn("⚠️ WARNING: No authentication configured! SDK will fail.");
|
logger.warn('⚠️ WARNING: No authentication configured! SDK will fail.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,16 +51,13 @@ export function logAuthStatus(context: string): void {
|
|||||||
*/
|
*/
|
||||||
export function logError(error: unknown, context: string): void {
|
export function logError(error: unknown, context: string): void {
|
||||||
logger.error(`❌ ${context}:`);
|
logger.error(`❌ ${context}:`);
|
||||||
logger.error("Error name:", (error as any)?.name);
|
logger.error('Error name:', (error as any)?.name);
|
||||||
logger.error("Error message:", (error as Error)?.message);
|
logger.error('Error message:', (error as Error)?.message);
|
||||||
logger.error("Error stack:", (error as Error)?.stack);
|
logger.error('Error stack:', (error as Error)?.stack);
|
||||||
logger.error(
|
logger.error('Full error object:', JSON.stringify(error, Object.getOwnPropertyNames(error), 2));
|
||||||
"Full error object:",
|
|
||||||
JSON.stringify(error, Object.getOwnPropertyNames(error), 2)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
import { getErrorMessage as getErrorMessageShared } from "../common.js";
|
import { getErrorMessage as getErrorMessageShared } from '../common.js';
|
||||||
|
|
||||||
// Re-export shared utility
|
// Re-export shared utility
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -2,25 +2,22 @@
|
|||||||
* Spec Regeneration routes - HTTP API for AI-powered spec generation
|
* Spec Regeneration routes - HTTP API for AI-powered spec generation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { createCreateHandler } from "./routes/create.js";
|
import { createCreateHandler } from './routes/create.js';
|
||||||
import { createGenerateHandler } from "./routes/generate.js";
|
import { createGenerateHandler } from './routes/generate.js';
|
||||||
import { createGenerateFeaturesHandler } from "./routes/generate-features.js";
|
import { createGenerateFeaturesHandler } from './routes/generate-features.js';
|
||||||
import { createStopHandler } from "./routes/stop.js";
|
import { createStopHandler } from './routes/stop.js';
|
||||||
import { createStatusHandler } from "./routes/status.js";
|
import { createStatusHandler } from './routes/status.js';
|
||||||
|
|
||||||
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/create", createCreateHandler(events));
|
router.post('/create', createCreateHandler(events));
|
||||||
router.post("/generate", createGenerateHandler(events));
|
router.post('/generate', createGenerateHandler(events));
|
||||||
router.post("/generate-features", createGenerateFeaturesHandler(events));
|
router.post('/generate-features', createGenerateFeaturesHandler(events));
|
||||||
router.post("/stop", createStopHandler());
|
router.post('/stop', createStopHandler());
|
||||||
router.get("/status", createStatusHandler());
|
router.get('/status', createStatusHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,24 +2,24 @@
|
|||||||
* POST /create endpoint - Create project spec from overview
|
* POST /create endpoint - Create project spec from overview
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { EventEmitter } from "../../../lib/events.js";
|
import type { EventEmitter } from '../../../lib/events.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import {
|
||||||
getSpecRegenerationStatus,
|
getSpecRegenerationStatus,
|
||||||
setRunningState,
|
setRunningState,
|
||||||
logAuthStatus,
|
logAuthStatus,
|
||||||
logError,
|
logError,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
} from "../common.js";
|
} from '../common.js';
|
||||||
import { generateSpec } from "../generate-spec.js";
|
import { generateSpec } from '../generate-spec.js';
|
||||||
|
|
||||||
const logger = createLogger("SpecRegeneration");
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
export function createCreateHandler(events: EventEmitter) {
|
export function createCreateHandler(events: EventEmitter) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
logger.info("========== /create endpoint called ==========");
|
logger.info('========== /create endpoint called ==========');
|
||||||
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
|
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath, projectOverview, generateFeatures, analyzeProject, maxFeatures } =
|
const { projectPath, projectOverview, generateFeatures, analyzeProject, maxFeatures } =
|
||||||
@@ -31,37 +31,34 @@ export function createCreateHandler(events: EventEmitter) {
|
|||||||
maxFeatures?: number;
|
maxFeatures?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug("Parsed params:");
|
logger.debug('Parsed params:');
|
||||||
logger.debug(" projectPath:", projectPath);
|
logger.debug(' projectPath:', projectPath);
|
||||||
logger.debug(
|
logger.debug(' projectOverview length:', `${projectOverview?.length || 0} chars`);
|
||||||
" projectOverview length:",
|
logger.debug(' generateFeatures:', generateFeatures);
|
||||||
`${projectOverview?.length || 0} chars`
|
logger.debug(' analyzeProject:', analyzeProject);
|
||||||
);
|
logger.debug(' maxFeatures:', maxFeatures);
|
||||||
logger.debug(" generateFeatures:", generateFeatures);
|
|
||||||
logger.debug(" analyzeProject:", analyzeProject);
|
|
||||||
logger.debug(" maxFeatures:", maxFeatures);
|
|
||||||
|
|
||||||
if (!projectPath || !projectOverview) {
|
if (!projectPath || !projectOverview) {
|
||||||
logger.error("Missing required parameters");
|
logger.error('Missing required parameters');
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath and projectOverview required",
|
error: 'projectPath and projectOverview required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isRunning } = getSpecRegenerationStatus();
|
const { isRunning } = getSpecRegenerationStatus();
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
logger.warn("Generation already running, rejecting request");
|
logger.warn('Generation already running, rejecting request');
|
||||||
res.json({ success: false, error: "Spec generation already running" });
|
res.json({ success: false, error: 'Spec generation already running' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logAuthStatus("Before starting generation");
|
logAuthStatus('Before starting generation');
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
setRunningState(true, abortController);
|
setRunningState(true, abortController);
|
||||||
logger.info("Starting background generation task...");
|
logger.info('Starting background generation task...');
|
||||||
|
|
||||||
// Start generation in background
|
// Start generation in background
|
||||||
generateSpec(
|
generateSpec(
|
||||||
@@ -74,24 +71,22 @@ export function createCreateHandler(events: EventEmitter) {
|
|||||||
maxFeatures
|
maxFeatures
|
||||||
)
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logError(error, "Generation failed with error");
|
logError(error, 'Generation failed with error');
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_error",
|
type: 'spec_regeneration_error',
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
logger.info("Generation task finished (success or error)");
|
logger.info('Generation task finished (success or error)');
|
||||||
setRunningState(false, null);
|
setRunningState(false, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info('Returning success response (generation running in background)');
|
||||||
"Returning success response (generation running in background)"
|
|
||||||
);
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Create spec route handler failed");
|
logError(error, 'Create spec route handler failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,24 +2,24 @@
|
|||||||
* POST /generate-features endpoint - Generate features from existing spec
|
* POST /generate-features endpoint - Generate features from existing spec
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { EventEmitter } from "../../../lib/events.js";
|
import type { EventEmitter } from '../../../lib/events.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import {
|
||||||
getSpecRegenerationStatus,
|
getSpecRegenerationStatus,
|
||||||
setRunningState,
|
setRunningState,
|
||||||
logAuthStatus,
|
logAuthStatus,
|
||||||
logError,
|
logError,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
} from "../common.js";
|
} from '../common.js';
|
||||||
import { generateFeaturesFromSpec } from "../generate-features-from-spec.js";
|
import { generateFeaturesFromSpec } from '../generate-features-from-spec.js';
|
||||||
|
|
||||||
const logger = createLogger("SpecRegeneration");
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
export function createGenerateFeaturesHandler(events: EventEmitter) {
|
export function createGenerateFeaturesHandler(events: EventEmitter) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
logger.info("========== /generate-features endpoint called ==========");
|
logger.info('========== /generate-features endpoint called ==========');
|
||||||
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
|
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { projectPath, maxFeatures } = req.body as {
|
const { projectPath, maxFeatures } = req.body as {
|
||||||
@@ -27,52 +27,45 @@ export function createGenerateFeaturesHandler(events: EventEmitter) {
|
|||||||
maxFeatures?: number;
|
maxFeatures?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug("projectPath:", projectPath);
|
logger.debug('projectPath:', projectPath);
|
||||||
logger.debug("maxFeatures:", maxFeatures);
|
logger.debug('maxFeatures:', maxFeatures);
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
logger.error("Missing projectPath parameter");
|
logger.error('Missing projectPath parameter');
|
||||||
res.status(400).json({ success: false, error: "projectPath required" });
|
res.status(400).json({ success: false, error: 'projectPath required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isRunning } = getSpecRegenerationStatus();
|
const { isRunning } = getSpecRegenerationStatus();
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
logger.warn("Generation already running, rejecting request");
|
logger.warn('Generation already running, rejecting request');
|
||||||
res.json({ success: false, error: "Generation already running" });
|
res.json({ success: false, error: 'Generation already running' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logAuthStatus("Before starting feature generation");
|
logAuthStatus('Before starting feature generation');
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
setRunningState(true, abortController);
|
setRunningState(true, abortController);
|
||||||
logger.info("Starting background feature generation task...");
|
logger.info('Starting background feature generation task...');
|
||||||
|
|
||||||
generateFeaturesFromSpec(
|
generateFeaturesFromSpec(projectPath, events, abortController, maxFeatures)
|
||||||
projectPath,
|
|
||||||
events,
|
|
||||||
abortController,
|
|
||||||
maxFeatures
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logError(error, "Feature generation failed with error");
|
logError(error, 'Feature generation failed with error');
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "features_error",
|
type: 'features_error',
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
logger.info("Feature generation task finished (success or error)");
|
logger.info('Feature generation task finished (success or error)');
|
||||||
setRunningState(false, null);
|
setRunningState(false, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info('Returning success response (generation running in background)');
|
||||||
"Returning success response (generation running in background)"
|
|
||||||
);
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Generate features route handler failed");
|
logError(error, 'Generate features route handler failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,71 +2,63 @@
|
|||||||
* POST /generate endpoint - Generate spec from project definition
|
* POST /generate endpoint - Generate spec from project definition
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { EventEmitter } from "../../../lib/events.js";
|
import type { EventEmitter } from '../../../lib/events.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import {
|
||||||
getSpecRegenerationStatus,
|
getSpecRegenerationStatus,
|
||||||
setRunningState,
|
setRunningState,
|
||||||
logAuthStatus,
|
logAuthStatus,
|
||||||
logError,
|
logError,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
} from "../common.js";
|
} from '../common.js';
|
||||||
import { generateSpec } from "../generate-spec.js";
|
import { generateSpec } from '../generate-spec.js';
|
||||||
|
|
||||||
const logger = createLogger("SpecRegeneration");
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
export function createGenerateHandler(events: EventEmitter) {
|
export function createGenerateHandler(events: EventEmitter) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
logger.info("========== /generate endpoint called ==========");
|
logger.info('========== /generate endpoint called ==========');
|
||||||
logger.debug("Request body:", JSON.stringify(req.body, null, 2));
|
logger.debug('Request body:', JSON.stringify(req.body, null, 2));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const {
|
const { projectPath, projectDefinition, generateFeatures, analyzeProject, maxFeatures } =
|
||||||
projectPath,
|
req.body as {
|
||||||
projectDefinition,
|
projectPath: string;
|
||||||
generateFeatures,
|
projectDefinition: string;
|
||||||
analyzeProject,
|
generateFeatures?: boolean;
|
||||||
maxFeatures,
|
analyzeProject?: boolean;
|
||||||
} = req.body as {
|
maxFeatures?: number;
|
||||||
projectPath: string;
|
};
|
||||||
projectDefinition: string;
|
|
||||||
generateFeatures?: boolean;
|
|
||||||
analyzeProject?: boolean;
|
|
||||||
maxFeatures?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug("Parsed params:");
|
logger.debug('Parsed params:');
|
||||||
logger.debug(" projectPath:", projectPath);
|
logger.debug(' projectPath:', projectPath);
|
||||||
logger.debug(
|
logger.debug(' projectDefinition length:', `${projectDefinition?.length || 0} chars`);
|
||||||
" projectDefinition length:",
|
logger.debug(' generateFeatures:', generateFeatures);
|
||||||
`${projectDefinition?.length || 0} chars`
|
logger.debug(' analyzeProject:', analyzeProject);
|
||||||
);
|
logger.debug(' maxFeatures:', maxFeatures);
|
||||||
logger.debug(" generateFeatures:", generateFeatures);
|
|
||||||
logger.debug(" analyzeProject:", analyzeProject);
|
|
||||||
logger.debug(" maxFeatures:", maxFeatures);
|
|
||||||
|
|
||||||
if (!projectPath || !projectDefinition) {
|
if (!projectPath || !projectDefinition) {
|
||||||
logger.error("Missing required parameters");
|
logger.error('Missing required parameters');
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath and projectDefinition required",
|
error: 'projectPath and projectDefinition required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { isRunning } = getSpecRegenerationStatus();
|
const { isRunning } = getSpecRegenerationStatus();
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
logger.warn("Generation already running, rejecting request");
|
logger.warn('Generation already running, rejecting request');
|
||||||
res.json({ success: false, error: "Spec generation already running" });
|
res.json({ success: false, error: 'Spec generation already running' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logAuthStatus("Before starting generation");
|
logAuthStatus('Before starting generation');
|
||||||
|
|
||||||
const abortController = new AbortController();
|
const abortController = new AbortController();
|
||||||
setRunningState(true, abortController);
|
setRunningState(true, abortController);
|
||||||
logger.info("Starting background generation task...");
|
logger.info('Starting background generation task...');
|
||||||
|
|
||||||
generateSpec(
|
generateSpec(
|
||||||
projectPath,
|
projectPath,
|
||||||
@@ -78,24 +70,22 @@ export function createGenerateHandler(events: EventEmitter) {
|
|||||||
maxFeatures
|
maxFeatures
|
||||||
)
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logError(error, "Generation failed with error");
|
logError(error, 'Generation failed with error');
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_error",
|
type: 'spec_regeneration_error',
|
||||||
error: getErrorMessage(error),
|
error: getErrorMessage(error),
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
logger.info("Generation task finished (success or error)");
|
logger.info('Generation task finished (success or error)');
|
||||||
setRunningState(false, null);
|
setRunningState(false, null);
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(
|
logger.info('Returning success response (generation running in background)');
|
||||||
"Returning success response (generation running in background)"
|
|
||||||
);
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Generate spec route handler failed");
|
logError(error, 'Generate spec route handler failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
* GET /status endpoint - Get generation status
|
* GET /status endpoint - Get generation status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { getSpecRegenerationStatus, getErrorMessage } from "../common.js";
|
import { getSpecRegenerationStatus, getErrorMessage } from '../common.js';
|
||||||
|
|
||||||
export function createStatusHandler() {
|
export function createStatusHandler() {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
|
|||||||
@@ -2,12 +2,8 @@
|
|||||||
* POST /stop endpoint - Stop generation
|
* POST /stop endpoint - Stop generation
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import {
|
import { getSpecRegenerationStatus, setRunningState, getErrorMessage } from '../common.js';
|
||||||
getSpecRegenerationStatus,
|
|
||||||
setRunningState,
|
|
||||||
getErrorMessage,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
export function createStopHandler() {
|
export function createStopHandler() {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
* Common utilities for auto-mode routes
|
* Common utilities for auto-mode routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("AutoMode");
|
const logger = createLogger('AutoMode');
|
||||||
|
|
||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -4,37 +4,65 @@
|
|||||||
* Uses the AutoModeService for real feature execution with Claude Agent SDK
|
* Uses the AutoModeService for real feature execution with Claude Agent SDK
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import type { AutoModeService } from "../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../services/auto-mode-service.js';
|
||||||
import { validatePathParams } from "../../middleware/validate-paths.js";
|
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||||
import { createStopFeatureHandler } from "./routes/stop-feature.js";
|
import { createStopFeatureHandler } from './routes/stop-feature.js';
|
||||||
import { createStatusHandler } from "./routes/status.js";
|
import { createStatusHandler } from './routes/status.js';
|
||||||
import { createRunFeatureHandler } from "./routes/run-feature.js";
|
import { createRunFeatureHandler } from './routes/run-feature.js';
|
||||||
import { createVerifyFeatureHandler } from "./routes/verify-feature.js";
|
import { createVerifyFeatureHandler } from './routes/verify-feature.js';
|
||||||
import { createResumeFeatureHandler } from "./routes/resume-feature.js";
|
import { createResumeFeatureHandler } from './routes/resume-feature.js';
|
||||||
import { createContextExistsHandler } from "./routes/context-exists.js";
|
import { createContextExistsHandler } from './routes/context-exists.js';
|
||||||
import { createAnalyzeProjectHandler } from "./routes/analyze-project.js";
|
import { createAnalyzeProjectHandler } from './routes/analyze-project.js';
|
||||||
import { createFollowUpFeatureHandler } from "./routes/follow-up-feature.js";
|
import { createFollowUpFeatureHandler } from './routes/follow-up-feature.js';
|
||||||
import { createCommitFeatureHandler } from "./routes/commit-feature.js";
|
import { createCommitFeatureHandler } from './routes/commit-feature.js';
|
||||||
import { createApprovePlanHandler } from "./routes/approve-plan.js";
|
import { createApprovePlanHandler } from './routes/approve-plan.js';
|
||||||
|
|
||||||
export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
|
export function createAutoModeRoutes(autoModeService: AutoModeService): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/stop-feature", createStopFeatureHandler(autoModeService));
|
router.post('/stop-feature', createStopFeatureHandler(autoModeService));
|
||||||
router.post("/status", validatePathParams("projectPath?"), createStatusHandler(autoModeService));
|
router.post('/status', validatePathParams('projectPath?'), createStatusHandler(autoModeService));
|
||||||
router.post("/run-feature", validatePathParams("projectPath"), createRunFeatureHandler(autoModeService));
|
|
||||||
router.post("/verify-feature", validatePathParams("projectPath"), createVerifyFeatureHandler(autoModeService));
|
|
||||||
router.post("/resume-feature", validatePathParams("projectPath"), createResumeFeatureHandler(autoModeService));
|
|
||||||
router.post("/context-exists", validatePathParams("projectPath"), createContextExistsHandler(autoModeService));
|
|
||||||
router.post("/analyze-project", validatePathParams("projectPath"), createAnalyzeProjectHandler(autoModeService));
|
|
||||||
router.post(
|
router.post(
|
||||||
"/follow-up-feature",
|
'/run-feature',
|
||||||
validatePathParams("projectPath", "imagePaths[]"),
|
validatePathParams('projectPath'),
|
||||||
|
createRunFeatureHandler(autoModeService)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/verify-feature',
|
||||||
|
validatePathParams('projectPath'),
|
||||||
|
createVerifyFeatureHandler(autoModeService)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/resume-feature',
|
||||||
|
validatePathParams('projectPath'),
|
||||||
|
createResumeFeatureHandler(autoModeService)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/context-exists',
|
||||||
|
validatePathParams('projectPath'),
|
||||||
|
createContextExistsHandler(autoModeService)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/analyze-project',
|
||||||
|
validatePathParams('projectPath'),
|
||||||
|
createAnalyzeProjectHandler(autoModeService)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/follow-up-feature',
|
||||||
|
validatePathParams('projectPath', 'imagePaths[]'),
|
||||||
createFollowUpFeatureHandler(autoModeService)
|
createFollowUpFeatureHandler(autoModeService)
|
||||||
);
|
);
|
||||||
router.post("/commit-feature", validatePathParams("projectPath", "worktreePath?"), createCommitFeatureHandler(autoModeService));
|
router.post(
|
||||||
router.post("/approve-plan", validatePathParams("projectPath"), createApprovePlanHandler(autoModeService));
|
'/commit-feature',
|
||||||
|
validatePathParams('projectPath', 'worktreePath?'),
|
||||||
|
createCommitFeatureHandler(autoModeService)
|
||||||
|
);
|
||||||
|
router.post(
|
||||||
|
'/approve-plan',
|
||||||
|
validatePathParams('projectPath'),
|
||||||
|
createApprovePlanHandler(autoModeService)
|
||||||
|
);
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* POST /analyze-project endpoint - Analyze project
|
* POST /analyze-project endpoint - Analyze project
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const logger = createLogger("AutoMode");
|
const logger = createLogger('AutoMode');
|
||||||
|
|
||||||
export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
|
export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -15,9 +15,7 @@ export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
|
|||||||
const { projectPath } = req.body as { projectPath: string };
|
const { projectPath } = req.body as { projectPath: string };
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "projectPath is required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -26,9 +24,9 @@ export function createAnalyzeProjectHandler(autoModeService: AutoModeService) {
|
|||||||
logger.error(`[AutoMode] Project analysis error:`, error);
|
logger.error(`[AutoMode] Project analysis error:`, error);
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ success: true, message: "Project analysis started" });
|
res.json({ success: true, message: 'Project analysis started' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Analyze project failed");
|
logError(error, 'Analyze project failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* POST /approve-plan endpoint - Approve or reject a generated plan/spec
|
* POST /approve-plan endpoint - Approve or reject a generated plan/spec
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const logger = createLogger("AutoMode");
|
const logger = createLogger('AutoMode');
|
||||||
|
|
||||||
export function createApprovePlanHandler(autoModeService: AutoModeService) {
|
export function createApprovePlanHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -23,15 +23,15 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
|
|||||||
if (!featureId) {
|
if (!featureId) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "featureId is required",
|
error: 'featureId is required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof approved !== "boolean") {
|
if (typeof approved !== 'boolean') {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "approved must be a boolean",
|
error: 'approved must be a boolean',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -41,9 +41,9 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
|
|||||||
// This supports cases where the server restarted while waiting for approval
|
// This supports cases where the server restarted while waiting for approval
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`[AutoMode] Plan ${approved ? "approved" : "rejected"} for feature ${featureId}${
|
`[AutoMode] Plan ${approved ? 'approved' : 'rejected'} for feature ${featureId}${
|
||||||
editedPlan ? " (with edits)" : ""
|
editedPlan ? ' (with edits)' : ''
|
||||||
}${feedback ? ` - Feedback: ${feedback}` : ""}`
|
}${feedback ? ` - Feedback: ${feedback}` : ''}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Resolve the pending approval (with recovery support)
|
// Resolve the pending approval (with recovery support)
|
||||||
@@ -67,11 +67,11 @@ export function createApprovePlanHandler(autoModeService: AutoModeService) {
|
|||||||
success: true,
|
success: true,
|
||||||
approved,
|
approved,
|
||||||
message: approved
|
message: approved
|
||||||
? "Plan approved - implementation will continue"
|
? 'Plan approved - implementation will continue'
|
||||||
: "Plan rejected - feature execution stopped",
|
: 'Plan rejected - feature execution stopped',
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Approve plan failed");
|
logError(error, 'Approve plan failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /commit-feature endpoint - Commit feature changes
|
* POST /commit-feature endpoint - Commit feature changes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createCommitFeatureHandler(autoModeService: AutoModeService) {
|
export function createCommitFeatureHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -16,23 +16,17 @@ export function createCommitFeatureHandler(autoModeService: AutoModeService) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res
|
res.status(400).json({
|
||||||
.status(400)
|
success: false,
|
||||||
.json({
|
error: 'projectPath and featureId are required',
|
||||||
success: false,
|
});
|
||||||
error: "projectPath and featureId are required",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const commitHash = await autoModeService.commitFeature(
|
const commitHash = await autoModeService.commitFeature(projectPath, featureId, worktreePath);
|
||||||
projectPath,
|
|
||||||
featureId,
|
|
||||||
worktreePath
|
|
||||||
);
|
|
||||||
res.json({ success: true, commitHash });
|
res.json({ success: true, commitHash });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Commit feature failed");
|
logError(error, 'Commit feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /context-exists endpoint - Check if context exists for a feature
|
* POST /context-exists endpoint - Check if context exists for a feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createContextExistsHandler(autoModeService: AutoModeService) {
|
export function createContextExistsHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -15,22 +15,17 @@ export function createContextExistsHandler(autoModeService: AutoModeService) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res
|
res.status(400).json({
|
||||||
.status(400)
|
success: false,
|
||||||
.json({
|
error: 'projectPath and featureId are required',
|
||||||
success: false,
|
});
|
||||||
error: "projectPath and featureId are required",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const exists = await autoModeService.contextExists(
|
const exists = await autoModeService.contextExists(projectPath, featureId);
|
||||||
projectPath,
|
|
||||||
featureId
|
|
||||||
);
|
|
||||||
res.json({ success: true, exists });
|
res.json({ success: true, exists });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Check context exists failed");
|
logError(error, 'Check context exists failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,29 +2,28 @@
|
|||||||
* POST /follow-up-feature endpoint - Follow up on a feature
|
* POST /follow-up-feature endpoint - Follow up on a feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const logger = createLogger("AutoMode");
|
const logger = createLogger('AutoMode');
|
||||||
|
|
||||||
export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
|
export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { projectPath, featureId, prompt, imagePaths, useWorktrees } =
|
const { projectPath, featureId, prompt, imagePaths, useWorktrees } = req.body as {
|
||||||
req.body as {
|
projectPath: string;
|
||||||
projectPath: string;
|
featureId: string;
|
||||||
featureId: string;
|
prompt: string;
|
||||||
prompt: string;
|
imagePaths?: string[];
|
||||||
imagePaths?: string[];
|
useWorktrees?: boolean;
|
||||||
useWorktrees?: boolean;
|
};
|
||||||
};
|
|
||||||
|
|
||||||
if (!projectPath || !featureId || !prompt) {
|
if (!projectPath || !featureId || !prompt) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath, featureId, and prompt are required",
|
error: 'projectPath, featureId, and prompt are required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -32,18 +31,9 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
|
|||||||
// Start follow-up in background
|
// Start follow-up in background
|
||||||
// followUpFeature derives workDir from feature.branchName
|
// followUpFeature derives workDir from feature.branchName
|
||||||
autoModeService
|
autoModeService
|
||||||
.followUpFeature(
|
.followUpFeature(projectPath, featureId, prompt, imagePaths, useWorktrees ?? true)
|
||||||
projectPath,
|
|
||||||
featureId,
|
|
||||||
prompt,
|
|
||||||
imagePaths,
|
|
||||||
useWorktrees ?? true
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
logger.error(
|
logger.error(`[AutoMode] Follow up feature ${featureId} error:`, error);
|
||||||
`[AutoMode] Follow up feature ${featureId} error:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
// Release the starting slot when follow-up completes (success or error)
|
// Release the starting slot when follow-up completes (success or error)
|
||||||
@@ -52,7 +42,7 @@ export function createFollowUpFeatureHandler(autoModeService: AutoModeService) {
|
|||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Follow up feature failed");
|
logError(error, 'Follow up feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* POST /resume-feature endpoint - Resume a feature
|
* POST /resume-feature endpoint - Resume a feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const logger = createLogger("AutoMode");
|
const logger = createLogger('AutoMode');
|
||||||
|
|
||||||
export function createResumeFeatureHandler(autoModeService: AutoModeService) {
|
export function createResumeFeatureHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -21,7 +21,7 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
|
|||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath and featureId are required",
|
error: 'projectPath and featureId are required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ export function createResumeFeatureHandler(autoModeService: AutoModeService) {
|
|||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Resume feature failed");
|
logError(error, 'Resume feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* POST /run-feature endpoint - Run a single feature
|
* POST /run-feature endpoint - Run a single feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const logger = createLogger("AutoMode");
|
const logger = createLogger('AutoMode');
|
||||||
|
|
||||||
export function createRunFeatureHandler(autoModeService: AutoModeService) {
|
export function createRunFeatureHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -21,7 +21,7 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) {
|
|||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath and featureId are required",
|
error: 'projectPath and featureId are required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@ export function createRunFeatureHandler(autoModeService: AutoModeService) {
|
|||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Run feature failed");
|
logError(error, 'Run feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /status endpoint - Get auto mode status
|
* POST /status endpoint - Get auto mode status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createStatusHandler(autoModeService: AutoModeService) {
|
export function createStatusHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -15,7 +15,7 @@ export function createStatusHandler(autoModeService: AutoModeService) {
|
|||||||
...status,
|
...status,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get status failed");
|
logError(error, 'Get status failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /stop-feature endpoint - Stop a specific feature
|
* POST /stop-feature endpoint - Stop a specific feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createStopFeatureHandler(autoModeService: AutoModeService) {
|
export function createStopFeatureHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -12,16 +12,14 @@ export function createStopFeatureHandler(autoModeService: AutoModeService) {
|
|||||||
const { featureId } = req.body as { featureId: string };
|
const { featureId } = req.body as { featureId: string };
|
||||||
|
|
||||||
if (!featureId) {
|
if (!featureId) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'featureId is required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "featureId is required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopped = await autoModeService.stopFeature(featureId);
|
const stopped = await autoModeService.stopFeature(featureId);
|
||||||
res.json({ success: true, stopped });
|
res.json({ success: true, stopped });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Stop feature failed");
|
logError(error, 'Stop feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /verify-feature endpoint - Verify a feature
|
* POST /verify-feature endpoint - Verify a feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
|
export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -15,22 +15,17 @@ export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res
|
res.status(400).json({
|
||||||
.status(400)
|
success: false,
|
||||||
.json({
|
error: 'projectPath and featureId are required',
|
||||||
success: false,
|
});
|
||||||
error: "projectPath and featureId are required",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const passes = await autoModeService.verifyFeature(
|
const passes = await autoModeService.verifyFeature(projectPath, featureId);
|
||||||
projectPath,
|
|
||||||
featureId
|
|
||||||
);
|
|
||||||
res.json({ success: true, passes });
|
res.json({ success: true, passes });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Verify feature failed");
|
logError(error, 'Verify feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { Router, Request, Response } from "express";
|
import { Router, Request, Response } from 'express';
|
||||||
import { ClaudeUsageService } from "../../services/claude-usage-service.js";
|
import { ClaudeUsageService } from '../../services/claude-usage-service.js';
|
||||||
|
|
||||||
export function createClaudeRoutes(service: ClaudeUsageService): Router {
|
export function createClaudeRoutes(service: ClaudeUsageService): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Get current usage (fetches from Claude CLI)
|
// Get current usage (fetches from Claude CLI)
|
||||||
router.get("/usage", async (req: Request, res: Response) => {
|
router.get('/usage', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Check if Claude CLI is available first
|
// Check if Claude CLI is available first
|
||||||
const isAvailable = await service.isAvailable();
|
const isAvailable = await service.isAvailable();
|
||||||
if (!isAvailable) {
|
if (!isAvailable) {
|
||||||
res.status(503).json({
|
res.status(503).json({
|
||||||
error: "Claude CLI not found",
|
error: 'Claude CLI not found',
|
||||||
message: "Please install Claude Code CLI and run 'claude login' to authenticate"
|
message: "Please install Claude Code CLI and run 'claude login' to authenticate",
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -20,20 +20,20 @@ export function createClaudeRoutes(service: ClaudeUsageService): Router {
|
|||||||
const usage = await service.fetchUsageData();
|
const usage = await service.fetchUsageData();
|
||||||
res.json(usage);
|
res.json(usage);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "Unknown error";
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
|
||||||
if (message.includes("Authentication required") || message.includes("token_expired")) {
|
if (message.includes('Authentication required') || message.includes('token_expired')) {
|
||||||
res.status(401).json({
|
res.status(401).json({
|
||||||
error: "Authentication required",
|
error: 'Authentication required',
|
||||||
message: "Please run 'claude login' to authenticate"
|
message: "Please run 'claude login' to authenticate",
|
||||||
});
|
});
|
||||||
} else if (message.includes("timed out")) {
|
} else if (message.includes('timed out')) {
|
||||||
res.status(504).json({
|
res.status(504).json({
|
||||||
error: "Command timed out",
|
error: 'Command timed out',
|
||||||
message: "The Claude CLI took too long to respond"
|
message: 'The Claude CLI took too long to respond',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("Error fetching usage:", error);
|
console.error('Error fetching usage:', error);
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export type ClaudeUsage = {
|
|||||||
|
|
||||||
export type ClaudeStatus = {
|
export type ClaudeStatus = {
|
||||||
indicator: {
|
indicator: {
|
||||||
color: "green" | "yellow" | "orange" | "red" | "gray";
|
color: 'green' | 'yellow' | 'orange' | 'red' | 'gray';
|
||||||
};
|
};
|
||||||
description: string;
|
description: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Common utilities shared across all route modules
|
* Common utilities shared across all route modules
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
|
|
||||||
// Re-export git utilities from shared package
|
// Re-export git utilities from shared package
|
||||||
export {
|
export {
|
||||||
@@ -16,7 +16,7 @@ export {
|
|||||||
listAllFilesInDirectory,
|
listAllFilesInDirectory,
|
||||||
generateDiffsForNonGitDirectory,
|
generateDiffsForNonGitDirectory,
|
||||||
getGitRepositoryDiffs,
|
getGitRepositoryDiffs,
|
||||||
} from "@automaker/git-utils";
|
} from '@automaker/git-utils';
|
||||||
|
|
||||||
type Logger = ReturnType<typeof createLogger>;
|
type Logger = ReturnType<typeof createLogger>;
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ type Logger = ReturnType<typeof createLogger>;
|
|||||||
* Get error message from error object
|
* Get error message from error object
|
||||||
*/
|
*/
|
||||||
export function getErrorMessage(error: unknown): string {
|
export function getErrorMessage(error: unknown): string {
|
||||||
return error instanceof Error ? error.message : "Unknown error";
|
return error instanceof Error ? error.message : 'Unknown error';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
24
apps/server/src/routes/context/index.ts
Normal file
24
apps/server/src/routes/context/index.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/**
|
||||||
|
* Context routes - HTTP API for context file operations
|
||||||
|
*
|
||||||
|
* Provides endpoints for managing context files including
|
||||||
|
* AI-powered image description generation.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Router } from 'express';
|
||||||
|
import { createDescribeImageHandler } from './routes/describe-image.js';
|
||||||
|
import { createDescribeFileHandler } from './routes/describe-file.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the context router
|
||||||
|
*
|
||||||
|
* @returns Express router with context endpoints
|
||||||
|
*/
|
||||||
|
export function createContextRoutes(): Router {
|
||||||
|
const router = Router();
|
||||||
|
|
||||||
|
router.post('/describe-image', createDescribeImageHandler());
|
||||||
|
router.post('/describe-file', createDescribeFileHandler());
|
||||||
|
|
||||||
|
return router;
|
||||||
|
}
|
||||||
220
apps/server/src/routes/context/routes/describe-file.ts
Normal file
220
apps/server/src/routes/context/routes/describe-file.ts
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
/**
|
||||||
|
* POST /context/describe-file endpoint - Generate description for a text file
|
||||||
|
*
|
||||||
|
* Uses Claude Haiku to analyze a text file and generate a concise description
|
||||||
|
* suitable for context file metadata.
|
||||||
|
*
|
||||||
|
* SECURITY: This endpoint validates file paths against ALLOWED_ROOT_DIRECTORY
|
||||||
|
* and reads file content directly (not via Claude's Read tool) to prevent
|
||||||
|
* arbitrary file reads and prompt injection attacks.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
|
import { createLogger } from '@automaker/utils';
|
||||||
|
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
||||||
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
|
import { createCustomOptions } from '../../../lib/sdk-options.js';
|
||||||
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const logger = createLogger('DescribeFile');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request body for the describe-file endpoint
|
||||||
|
*/
|
||||||
|
interface DescribeFileRequestBody {
|
||||||
|
/** Path to the file */
|
||||||
|
filePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success response from the describe-file endpoint
|
||||||
|
*/
|
||||||
|
interface DescribeFileSuccessResponse {
|
||||||
|
success: true;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error response from the describe-file endpoint
|
||||||
|
*/
|
||||||
|
interface DescribeFileErrorResponse {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract text content from Claude SDK response messages
|
||||||
|
*/
|
||||||
|
async function extractTextFromStream(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
stream: AsyncIterable<any>
|
||||||
|
): Promise<string> {
|
||||||
|
let responseText = '';
|
||||||
|
|
||||||
|
for await (const msg of stream) {
|
||||||
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
|
const blocks = msg.message.content as Array<{ type: string; text?: string }>;
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (block.type === 'text' && block.text) {
|
||||||
|
responseText += block.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
||||||
|
responseText = msg.result || responseText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the describe-file request handler
|
||||||
|
*
|
||||||
|
* @returns Express request handler for file description
|
||||||
|
*/
|
||||||
|
export function createDescribeFileHandler(): (req: Request, res: Response) => Promise<void> {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { filePath } = req.body as DescribeFileRequestBody;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!filePath || typeof filePath !== 'string') {
|
||||||
|
const response: DescribeFileErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: 'filePath is required and must be a string',
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[DescribeFile] Starting description generation for: ${filePath}`);
|
||||||
|
|
||||||
|
// Resolve the path for logging and cwd derivation
|
||||||
|
const resolvedPath = secureFs.resolvePath(filePath);
|
||||||
|
|
||||||
|
// Read file content using secureFs (validates path against ALLOWED_ROOT_DIRECTORY)
|
||||||
|
// This prevents arbitrary file reads (e.g., /etc/passwd, ~/.ssh/id_rsa)
|
||||||
|
// and prompt injection attacks where malicious filePath values could inject instructions
|
||||||
|
let fileContent: string;
|
||||||
|
try {
|
||||||
|
const content = await secureFs.readFile(resolvedPath, 'utf-8');
|
||||||
|
fileContent = typeof content === 'string' ? content : content.toString('utf-8');
|
||||||
|
} catch (readError) {
|
||||||
|
// Path not allowed - return 403 Forbidden
|
||||||
|
if (readError instanceof PathNotAllowedError) {
|
||||||
|
logger.warn(`[DescribeFile] Path not allowed: ${filePath}`);
|
||||||
|
const response: DescribeFileErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: 'File path is not within the allowed directory',
|
||||||
|
};
|
||||||
|
res.status(403).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File not found
|
||||||
|
if (
|
||||||
|
readError !== null &&
|
||||||
|
typeof readError === 'object' &&
|
||||||
|
'code' in readError &&
|
||||||
|
readError.code === 'ENOENT'
|
||||||
|
) {
|
||||||
|
logger.warn(`[DescribeFile] File not found: ${resolvedPath}`);
|
||||||
|
const response: DescribeFileErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: `File not found: ${filePath}`,
|
||||||
|
};
|
||||||
|
res.status(404).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const errorMessage = readError instanceof Error ? readError.message : 'Unknown error';
|
||||||
|
logger.error(`[DescribeFile] Failed to read file: ${errorMessage}`);
|
||||||
|
const response: DescribeFileErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to read file: ${errorMessage}`,
|
||||||
|
};
|
||||||
|
res.status(500).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Truncate very large files to avoid token limits
|
||||||
|
const MAX_CONTENT_LENGTH = 50000;
|
||||||
|
const truncated = fileContent.length > MAX_CONTENT_LENGTH;
|
||||||
|
const contentToAnalyze = truncated
|
||||||
|
? fileContent.substring(0, MAX_CONTENT_LENGTH)
|
||||||
|
: fileContent;
|
||||||
|
|
||||||
|
// Get the filename for context
|
||||||
|
const fileName = path.basename(resolvedPath);
|
||||||
|
|
||||||
|
// Build prompt with file content passed as structured data
|
||||||
|
// The file content is included directly, not via tool invocation
|
||||||
|
const instructionText = `Analyze the following file and provide a 1-2 sentence description suitable for use as context in an AI coding assistant. Focus on what the file contains, its purpose, and why an AI agent might want to use this context in the future (e.g., "API documentation for the authentication endpoints", "Configuration file for database connections", "Coding style guidelines for the project").
|
||||||
|
|
||||||
|
Respond with ONLY the description text, no additional formatting, preamble, or explanation.
|
||||||
|
|
||||||
|
File: ${fileName}${truncated ? ' (truncated)' : ''}`;
|
||||||
|
|
||||||
|
const promptContent = [
|
||||||
|
{ type: 'text' as const, text: instructionText },
|
||||||
|
{ type: 'text' as const, text: `\n\n--- FILE CONTENT ---\n${contentToAnalyze}` },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Use the file's directory as the working directory
|
||||||
|
const cwd = path.dirname(resolvedPath);
|
||||||
|
|
||||||
|
// Use centralized SDK options with proper cwd validation
|
||||||
|
// No tools needed since we're passing file content directly
|
||||||
|
const sdkOptions = createCustomOptions({
|
||||||
|
cwd,
|
||||||
|
model: CLAUDE_MODEL_MAP.haiku,
|
||||||
|
maxTurns: 1,
|
||||||
|
allowedTools: [],
|
||||||
|
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const promptGenerator = (async function* () {
|
||||||
|
yield {
|
||||||
|
type: 'user' as const,
|
||||||
|
session_id: '',
|
||||||
|
message: { role: 'user' as const, content: promptContent },
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
const stream = query({ prompt: promptGenerator, options: sdkOptions });
|
||||||
|
|
||||||
|
// Extract the description from the response
|
||||||
|
const description = await extractTextFromStream(stream);
|
||||||
|
|
||||||
|
if (!description || description.trim().length === 0) {
|
||||||
|
logger.warn('Received empty response from Claude');
|
||||||
|
const response: DescribeFileErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to generate description - empty response',
|
||||||
|
};
|
||||||
|
res.status(500).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Description generated, length: ${description.length} chars`);
|
||||||
|
|
||||||
|
const response: DescribeFileSuccessResponse = {
|
||||||
|
success: true,
|
||||||
|
description: description.trim(),
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
|
logger.error('File description failed:', errorMessage);
|
||||||
|
|
||||||
|
const response: DescribeFileErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
res.status(500).json(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
416
apps/server/src/routes/context/routes/describe-image.ts
Normal file
416
apps/server/src/routes/context/routes/describe-image.ts
Normal file
@@ -0,0 +1,416 @@
|
|||||||
|
/**
|
||||||
|
* POST /context/describe-image endpoint - Generate description for an image
|
||||||
|
*
|
||||||
|
* Uses Claude Haiku to analyze an image and generate a concise description
|
||||||
|
* suitable for context file metadata.
|
||||||
|
*
|
||||||
|
* IMPORTANT:
|
||||||
|
* The agent runner (chat/auto-mode) sends images as multi-part content blocks (base64 image blocks),
|
||||||
|
* not by asking Claude to use the Read tool to open files. This endpoint now mirrors that approach
|
||||||
|
* so it doesn't depend on Claude's filesystem tool access or working directory restrictions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
|
import { createLogger, readImageAsBase64 } from '@automaker/utils';
|
||||||
|
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
||||||
|
import { createCustomOptions } from '../../../lib/sdk-options.js';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
|
const logger = createLogger('DescribeImage');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allowlist of safe headers to log
|
||||||
|
* All other headers are excluded to prevent leaking sensitive values
|
||||||
|
*/
|
||||||
|
const SAFE_HEADERS_ALLOWLIST = new Set([
|
||||||
|
'content-type',
|
||||||
|
'accept',
|
||||||
|
'user-agent',
|
||||||
|
'host',
|
||||||
|
'referer',
|
||||||
|
'content-length',
|
||||||
|
'origin',
|
||||||
|
'x-request-id',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter request headers to only include safe, non-sensitive values
|
||||||
|
*/
|
||||||
|
function filterSafeHeaders(headers: Record<string, unknown>): Record<string, unknown> {
|
||||||
|
const filtered: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
if (SAFE_HEADERS_ALLOWLIST.has(key.toLowerCase())) {
|
||||||
|
filtered[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filtered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the actual file path, handling Unicode character variations.
|
||||||
|
* macOS screenshots use U+202F (NARROW NO-BREAK SPACE) before AM/PM,
|
||||||
|
* but this may be transmitted as a regular space through the API.
|
||||||
|
*/
|
||||||
|
function findActualFilePath(requestedPath: string): string | null {
|
||||||
|
// First, try the exact path
|
||||||
|
if (fs.existsSync(requestedPath)) {
|
||||||
|
return requestedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try with Unicode normalization
|
||||||
|
const normalizedPath = requestedPath.normalize('NFC');
|
||||||
|
if (fs.existsSync(normalizedPath)) {
|
||||||
|
return normalizedPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found, try to find the file in the directory by matching the basename
|
||||||
|
// This handles cases where the space character differs (U+0020 vs U+202F vs U+00A0)
|
||||||
|
const dir = path.dirname(requestedPath);
|
||||||
|
const baseName = path.basename(requestedPath);
|
||||||
|
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const files = fs.readdirSync(dir);
|
||||||
|
|
||||||
|
// Normalize the requested basename for comparison
|
||||||
|
// Replace various space-like characters with regular space for comparison
|
||||||
|
const normalizeSpaces = (s: string): string => s.replace(/[\u00A0\u202F\u2009\u200A]/g, ' ');
|
||||||
|
|
||||||
|
const normalizedBaseName = normalizeSpaces(baseName);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (normalizeSpaces(file) === normalizedBaseName) {
|
||||||
|
logger.info(`Found matching file with different space encoding: ${file}`);
|
||||||
|
return path.join(dir, file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(`Error reading directory ${dir}: ${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request body for the describe-image endpoint
|
||||||
|
*/
|
||||||
|
interface DescribeImageRequestBody {
|
||||||
|
/** Path to the image file */
|
||||||
|
imagePath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Success response from the describe-image endpoint
|
||||||
|
*/
|
||||||
|
interface DescribeImageSuccessResponse {
|
||||||
|
success: true;
|
||||||
|
description: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Error response from the describe-image endpoint
|
||||||
|
*/
|
||||||
|
interface DescribeImageErrorResponse {
|
||||||
|
success: false;
|
||||||
|
error: string;
|
||||||
|
requestId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map SDK/CLI errors to a stable status + user-facing message.
|
||||||
|
*/
|
||||||
|
function mapDescribeImageError(rawMessage: string | undefined): {
|
||||||
|
statusCode: number;
|
||||||
|
userMessage: string;
|
||||||
|
} {
|
||||||
|
const baseResponse = {
|
||||||
|
statusCode: 500,
|
||||||
|
userMessage: 'Failed to generate an image description. Please try again.',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!rawMessage) return baseResponse;
|
||||||
|
|
||||||
|
if (rawMessage.includes('Claude Code process exited')) {
|
||||||
|
return {
|
||||||
|
statusCode: 503,
|
||||||
|
userMessage:
|
||||||
|
'Claude exited unexpectedly while describing the image. Try again. If it keeps happening, re-run `claude login` or update your API key in Setup so Claude can restart cleanly.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
rawMessage.includes('Failed to spawn Claude Code process') ||
|
||||||
|
rawMessage.includes('Claude Code executable not found') ||
|
||||||
|
rawMessage.includes('Claude Code native binary not found')
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
statusCode: 503,
|
||||||
|
userMessage:
|
||||||
|
'Claude CLI could not be launched. Make sure the Claude CLI is installed and available in PATH, then try again.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawMessage.toLowerCase().includes('rate limit') || rawMessage.includes('429')) {
|
||||||
|
return {
|
||||||
|
statusCode: 429,
|
||||||
|
userMessage: 'Rate limited while describing the image. Please wait a moment and try again.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawMessage.toLowerCase().includes('payload too large') || rawMessage.includes('413')) {
|
||||||
|
return {
|
||||||
|
statusCode: 413,
|
||||||
|
userMessage:
|
||||||
|
'The image is too large to send for description. Please resize/compress it and try again.',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract text content from Claude SDK response messages and log high-signal stream events.
|
||||||
|
*/
|
||||||
|
async function extractTextFromStream(
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
stream: AsyncIterable<any>,
|
||||||
|
requestId: string
|
||||||
|
): Promise<string> {
|
||||||
|
let responseText = '';
|
||||||
|
let messageCount = 0;
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] [Stream] Begin reading SDK stream...`);
|
||||||
|
|
||||||
|
for await (const msg of stream) {
|
||||||
|
messageCount++;
|
||||||
|
const msgType = msg?.type;
|
||||||
|
const msgSubtype = msg?.subtype;
|
||||||
|
|
||||||
|
// Keep this concise but informative. Full error object is logged in catch blocks.
|
||||||
|
logger.info(
|
||||||
|
`[${requestId}] [Stream] #${messageCount} type=${String(msgType)} subtype=${String(msgSubtype ?? '')}`
|
||||||
|
);
|
||||||
|
|
||||||
|
if (msgType === 'assistant' && msg.message?.content) {
|
||||||
|
const blocks = msg.message.content as Array<{ type: string; text?: string }>;
|
||||||
|
logger.info(`[${requestId}] [Stream] assistant blocks=${blocks.length}`);
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (block.type === 'text' && block.text) {
|
||||||
|
responseText += block.text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msgType === 'result' && msgSubtype === 'success') {
|
||||||
|
if (typeof msg.result === 'string' && msg.result.length > 0) {
|
||||||
|
responseText = msg.result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`[${requestId}] [Stream] End of stream. messages=${messageCount} textLength=${responseText.length}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return responseText;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create the describe-image request handler
|
||||||
|
*
|
||||||
|
* Uses Claude SDK query with multi-part content blocks to include the image (base64),
|
||||||
|
* matching the agent runner behavior.
|
||||||
|
*
|
||||||
|
* @returns Express request handler for image description
|
||||||
|
*/
|
||||||
|
export function createDescribeImageHandler(): (req: Request, res: Response) => Promise<void> {
|
||||||
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const requestId = `describe-image-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
||||||
|
const startedAt = Date.now();
|
||||||
|
|
||||||
|
// Request envelope logs (high value when correlating failures)
|
||||||
|
// Only log safe headers to prevent leaking sensitive values (auth tokens, cookies, etc.)
|
||||||
|
logger.info(`[${requestId}] ===== POST /api/context/describe-image =====`);
|
||||||
|
logger.info(`[${requestId}] headers=${JSON.stringify(filterSafeHeaders(req.headers))}`);
|
||||||
|
logger.info(`[${requestId}] body=${JSON.stringify(req.body)}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { imagePath } = req.body as DescribeImageRequestBody;
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
if (!imagePath || typeof imagePath !== 'string') {
|
||||||
|
const response: DescribeImageErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: 'imagePath is required and must be a string',
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
res.status(400).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] imagePath="${imagePath}" type=${typeof imagePath}`);
|
||||||
|
|
||||||
|
// Find the actual file path (handles Unicode space character variations)
|
||||||
|
const actualPath = findActualFilePath(imagePath);
|
||||||
|
if (!actualPath) {
|
||||||
|
logger.error(`[${requestId}] File not found: ${imagePath}`);
|
||||||
|
// Log hex representation of the path for debugging
|
||||||
|
const hexPath = Buffer.from(imagePath).toString('hex');
|
||||||
|
logger.error(`[${requestId}] imagePath hex: ${hexPath}`);
|
||||||
|
const response: DescribeImageErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: `File not found: ${imagePath}`,
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
res.status(404).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actualPath !== imagePath) {
|
||||||
|
logger.info(`[${requestId}] Using actual path: ${actualPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log path + stats (this is often where issues start: missing file, perms, size)
|
||||||
|
let stat: fs.Stats | null = null;
|
||||||
|
try {
|
||||||
|
stat = fs.statSync(actualPath);
|
||||||
|
logger.info(
|
||||||
|
`[${requestId}] fileStats size=${stat.size} bytes mtime=${stat.mtime.toISOString()}`
|
||||||
|
);
|
||||||
|
} catch (statErr) {
|
||||||
|
logger.warn(
|
||||||
|
`[${requestId}] Unable to stat image file (continuing to read base64): ${String(statErr)}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read image and convert to base64 (same as agent runner)
|
||||||
|
logger.info(`[${requestId}] Reading image into base64...`);
|
||||||
|
const imageReadStart = Date.now();
|
||||||
|
const imageData = await readImageAsBase64(actualPath);
|
||||||
|
const imageReadMs = Date.now() - imageReadStart;
|
||||||
|
|
||||||
|
const base64Length = imageData.base64.length;
|
||||||
|
const estimatedBytes = Math.ceil((base64Length * 3) / 4);
|
||||||
|
logger.info(`[${requestId}] imageReadMs=${imageReadMs}`);
|
||||||
|
logger.info(
|
||||||
|
`[${requestId}] image meta filename=${imageData.filename} mime=${imageData.mimeType} base64Len=${base64Length} estBytes=${estimatedBytes}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Build multi-part prompt with image block (no Read tool required)
|
||||||
|
const instructionText =
|
||||||
|
`Describe this image in 1-2 sentences suitable for use as context in an AI coding assistant. ` +
|
||||||
|
`Focus on what the image shows and its purpose (e.g., "UI mockup showing login form with email/password fields", ` +
|
||||||
|
`"Architecture diagram of microservices", "Screenshot of error message in terminal").\n\n` +
|
||||||
|
`Respond with ONLY the description text, no additional formatting, preamble, or explanation.`;
|
||||||
|
|
||||||
|
const promptContent = [
|
||||||
|
{ type: 'text' as const, text: instructionText },
|
||||||
|
{
|
||||||
|
type: 'image' as const,
|
||||||
|
source: {
|
||||||
|
type: 'base64' as const,
|
||||||
|
media_type: imageData.mimeType,
|
||||||
|
data: imageData.base64,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Built multi-part prompt blocks=${promptContent.length}`);
|
||||||
|
|
||||||
|
const cwd = path.dirname(actualPath);
|
||||||
|
logger.info(`[${requestId}] Using cwd=${cwd}`);
|
||||||
|
|
||||||
|
// Use the same centralized option builder used across the server (validates cwd)
|
||||||
|
const sdkOptions = createCustomOptions({
|
||||||
|
cwd,
|
||||||
|
model: CLAUDE_MODEL_MAP.haiku,
|
||||||
|
maxTurns: 1,
|
||||||
|
allowedTools: [],
|
||||||
|
sandbox: { enabled: true, autoAllowBashIfSandboxed: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
`[${requestId}] SDK options model=${sdkOptions.model} maxTurns=${sdkOptions.maxTurns} allowedTools=${JSON.stringify(
|
||||||
|
sdkOptions.allowedTools
|
||||||
|
)} sandbox=${JSON.stringify(sdkOptions.sandbox)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
const promptGenerator = (async function* () {
|
||||||
|
yield {
|
||||||
|
type: 'user' as const,
|
||||||
|
session_id: '',
|
||||||
|
message: { role: 'user' as const, content: promptContent },
|
||||||
|
parent_tool_use_id: null,
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
|
||||||
|
logger.info(`[${requestId}] Calling query()...`);
|
||||||
|
const queryStart = Date.now();
|
||||||
|
const stream = query({ prompt: promptGenerator, options: sdkOptions });
|
||||||
|
logger.info(`[${requestId}] query() returned stream in ${Date.now() - queryStart}ms`);
|
||||||
|
|
||||||
|
// Extract the description from the response
|
||||||
|
const extractStart = Date.now();
|
||||||
|
const description = await extractTextFromStream(stream, requestId);
|
||||||
|
logger.info(`[${requestId}] extractMs=${Date.now() - extractStart}`);
|
||||||
|
|
||||||
|
if (!description || description.trim().length === 0) {
|
||||||
|
logger.warn(`[${requestId}] Received empty response from Claude`);
|
||||||
|
const response: DescribeImageErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to generate description - empty response',
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
res.status(500).json(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const totalMs = Date.now() - startedAt;
|
||||||
|
logger.info(`[${requestId}] Success descriptionLen=${description.length} totalMs=${totalMs}`);
|
||||||
|
|
||||||
|
const response: DescribeImageSuccessResponse = {
|
||||||
|
success: true,
|
||||||
|
description: description.trim(),
|
||||||
|
};
|
||||||
|
res.json(response);
|
||||||
|
} catch (error) {
|
||||||
|
const totalMs = Date.now() - startedAt;
|
||||||
|
const err = error as unknown;
|
||||||
|
const errMessage = err instanceof Error ? err.message : String(err);
|
||||||
|
const errName = err instanceof Error ? err.name : 'UnknownError';
|
||||||
|
const errStack = err instanceof Error ? err.stack : undefined;
|
||||||
|
|
||||||
|
logger.error(`[${requestId}] FAILED totalMs=${totalMs}`);
|
||||||
|
logger.error(`[${requestId}] errorName=${errName}`);
|
||||||
|
logger.error(`[${requestId}] errorMessage=${errMessage}`);
|
||||||
|
if (errStack) logger.error(`[${requestId}] errorStack=${errStack}`);
|
||||||
|
|
||||||
|
// Dump all enumerable + non-enumerable props (this is where stderr/stdout/exitCode often live)
|
||||||
|
try {
|
||||||
|
const props = err && typeof err === 'object' ? Object.getOwnPropertyNames(err) : [];
|
||||||
|
logger.error(`[${requestId}] errorProps=${JSON.stringify(props)}`);
|
||||||
|
if (err && typeof err === 'object') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const anyErr = err as any;
|
||||||
|
const details = JSON.stringify(anyErr, props as unknown as string[]);
|
||||||
|
logger.error(`[${requestId}] errorDetails=${details}`);
|
||||||
|
}
|
||||||
|
} catch (stringifyErr) {
|
||||||
|
logger.error(`[${requestId}] Failed to serialize error object: ${String(stringifyErr)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { statusCode, userMessage } = mapDescribeImageError(errMessage);
|
||||||
|
const response: DescribeImageErrorResponse = {
|
||||||
|
success: false,
|
||||||
|
error: `${userMessage} (requestId: ${requestId})`,
|
||||||
|
requestId,
|
||||||
|
};
|
||||||
|
res.status(statusCode).json(response);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
* with different enhancement modes (improve, expand, simplify, etc.)
|
* with different enhancement modes (improve, expand, simplify, etc.)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import { createEnhanceHandler } from "./routes/enhance.js";
|
import { createEnhanceHandler } from './routes/enhance.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the enhance-prompt router
|
* Create the enhance-prompt router
|
||||||
@@ -16,7 +16,7 @@ import { createEnhanceHandler } from "./routes/enhance.js";
|
|||||||
export function createEnhancePromptRoutes(): Router {
|
export function createEnhancePromptRoutes(): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/", createEnhanceHandler());
|
router.post('/', createEnhanceHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,19 @@
|
|||||||
* Supports modes: improve, technical, simplify, acceptance
|
* Supports modes: improve, technical, simplify, acceptance
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { resolveModelString } from "@automaker/model-resolver";
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
import { CLAUDE_MODEL_MAP } from "@automaker/types";
|
import { CLAUDE_MODEL_MAP } from '@automaker/types';
|
||||||
import {
|
import {
|
||||||
getSystemPrompt,
|
getSystemPrompt,
|
||||||
buildUserPrompt,
|
buildUserPrompt,
|
||||||
isValidEnhancementMode,
|
isValidEnhancementMode,
|
||||||
type EnhancementMode,
|
type EnhancementMode,
|
||||||
} from "../../../lib/enhancement-prompts.js";
|
} from '../../../lib/enhancement-prompts.js';
|
||||||
|
|
||||||
const logger = createLogger("EnhancePrompt");
|
const logger = createLogger('EnhancePrompt');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Request body for the enhance endpoint
|
* Request body for the enhance endpoint
|
||||||
@@ -63,16 +63,16 @@ async function extractTextFromStream(
|
|||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
let responseText = "";
|
let responseText = '';
|
||||||
|
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
if (msg.type === "assistant" && msg.message?.content) {
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text" && block.text) {
|
if (block.type === 'text' && block.text) {
|
||||||
responseText += block.text;
|
responseText += block.text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === "result" && msg.subtype === "success") {
|
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
||||||
responseText = msg.result || responseText;
|
responseText = msg.result || responseText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,29 +85,25 @@ async function extractTextFromStream(
|
|||||||
*
|
*
|
||||||
* @returns Express request handler for text enhancement
|
* @returns Express request handler for text enhancement
|
||||||
*/
|
*/
|
||||||
export function createEnhanceHandler(): (
|
export function createEnhanceHandler(): (req: Request, res: Response) => Promise<void> {
|
||||||
req: Request,
|
|
||||||
res: Response
|
|
||||||
) => Promise<void> {
|
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { originalText, enhancementMode, model } =
|
const { originalText, enhancementMode, model } = req.body as EnhanceRequestBody;
|
||||||
req.body as EnhanceRequestBody;
|
|
||||||
|
|
||||||
// Validate required fields
|
// Validate required fields
|
||||||
if (!originalText || typeof originalText !== "string") {
|
if (!originalText || typeof originalText !== 'string') {
|
||||||
const response: EnhanceErrorResponse = {
|
const response: EnhanceErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "originalText is required and must be a string",
|
error: 'originalText is required and must be a string',
|
||||||
};
|
};
|
||||||
res.status(400).json(response);
|
res.status(400).json(response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!enhancementMode || typeof enhancementMode !== "string") {
|
if (!enhancementMode || typeof enhancementMode !== 'string') {
|
||||||
const response: EnhanceErrorResponse = {
|
const response: EnhanceErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "enhancementMode is required and must be a string",
|
error: 'enhancementMode is required and must be a string',
|
||||||
};
|
};
|
||||||
res.status(400).json(response);
|
res.status(400).json(response);
|
||||||
return;
|
return;
|
||||||
@@ -118,7 +114,7 @@ export function createEnhanceHandler(): (
|
|||||||
if (trimmedText.length === 0) {
|
if (trimmedText.length === 0) {
|
||||||
const response: EnhanceErrorResponse = {
|
const response: EnhanceErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "originalText cannot be empty",
|
error: 'originalText cannot be empty',
|
||||||
};
|
};
|
||||||
res.status(400).json(response);
|
res.status(400).json(response);
|
||||||
return;
|
return;
|
||||||
@@ -128,11 +124,9 @@ export function createEnhanceHandler(): (
|
|||||||
const normalizedMode = enhancementMode.toLowerCase();
|
const normalizedMode = enhancementMode.toLowerCase();
|
||||||
const validMode: EnhancementMode = isValidEnhancementMode(normalizedMode)
|
const validMode: EnhancementMode = isValidEnhancementMode(normalizedMode)
|
||||||
? normalizedMode
|
? normalizedMode
|
||||||
: "improve";
|
: 'improve';
|
||||||
|
|
||||||
logger.info(
|
logger.info(`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`);
|
||||||
`Enhancing text with mode: ${validMode}, length: ${trimmedText.length} chars`
|
|
||||||
);
|
|
||||||
|
|
||||||
// Get the system prompt for this mode
|
// Get the system prompt for this mode
|
||||||
const systemPrompt = getSystemPrompt(validMode);
|
const systemPrompt = getSystemPrompt(validMode);
|
||||||
@@ -155,7 +149,7 @@ export function createEnhanceHandler(): (
|
|||||||
systemPrompt,
|
systemPrompt,
|
||||||
maxTurns: 1,
|
maxTurns: 1,
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
permissionMode: "acceptEdits",
|
permissionMode: 'acceptEdits',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -163,18 +157,16 @@ export function createEnhanceHandler(): (
|
|||||||
const enhancedText = await extractTextFromStream(stream);
|
const enhancedText = await extractTextFromStream(stream);
|
||||||
|
|
||||||
if (!enhancedText || enhancedText.trim().length === 0) {
|
if (!enhancedText || enhancedText.trim().length === 0) {
|
||||||
logger.warn("Received empty response from Claude");
|
logger.warn('Received empty response from Claude');
|
||||||
const response: EnhanceErrorResponse = {
|
const response: EnhanceErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Failed to generate enhanced text - empty response",
|
error: 'Failed to generate enhanced text - empty response',
|
||||||
};
|
};
|
||||||
res.status(500).json(response);
|
res.status(500).json(response);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(`Enhancement complete, output length: ${enhancedText.length} chars`);
|
||||||
`Enhancement complete, output length: ${enhancedText.length} chars`
|
|
||||||
);
|
|
||||||
|
|
||||||
const response: EnhanceSuccessResponse = {
|
const response: EnhanceSuccessResponse = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -182,9 +174,8 @@ export function createEnhanceHandler(): (
|
|||||||
};
|
};
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
error instanceof Error ? error.message : "Unknown error occurred";
|
logger.error('Enhancement failed:', errorMessage);
|
||||||
logger.error("Enhancement failed:", errorMessage);
|
|
||||||
|
|
||||||
const response: EnhanceErrorResponse = {
|
const response: EnhanceErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
* Common utilities for features routes
|
* Common utilities for features routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("Features");
|
const logger = createLogger('Features');
|
||||||
|
|
||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -2,27 +2,27 @@
|
|||||||
* Features routes - HTTP API for feature management
|
* Features routes - HTTP API for feature management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import { FeatureLoader } from "../../services/feature-loader.js";
|
import { FeatureLoader } from '../../services/feature-loader.js';
|
||||||
import { validatePathParams } from "../../middleware/validate-paths.js";
|
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||||
import { createListHandler } from "./routes/list.js";
|
import { createListHandler } from './routes/list.js';
|
||||||
import { createGetHandler } from "./routes/get.js";
|
import { createGetHandler } from './routes/get.js';
|
||||||
import { createCreateHandler } from "./routes/create.js";
|
import { createCreateHandler } from './routes/create.js';
|
||||||
import { createUpdateHandler } from "./routes/update.js";
|
import { createUpdateHandler } from './routes/update.js';
|
||||||
import { createDeleteHandler } from "./routes/delete.js";
|
import { createDeleteHandler } from './routes/delete.js';
|
||||||
import { createAgentOutputHandler } from "./routes/agent-output.js";
|
import { createAgentOutputHandler } from './routes/agent-output.js';
|
||||||
import { createGenerateTitleHandler } from "./routes/generate-title.js";
|
import { createGenerateTitleHandler } from './routes/generate-title.js';
|
||||||
|
|
||||||
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
|
export function createFeaturesRoutes(featureLoader: FeatureLoader): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/list", validatePathParams("projectPath"), createListHandler(featureLoader));
|
router.post('/list', validatePathParams('projectPath'), createListHandler(featureLoader));
|
||||||
router.post("/get", validatePathParams("projectPath"), createGetHandler(featureLoader));
|
router.post('/get', validatePathParams('projectPath'), createGetHandler(featureLoader));
|
||||||
router.post("/create", validatePathParams("projectPath"), createCreateHandler(featureLoader));
|
router.post('/create', validatePathParams('projectPath'), createCreateHandler(featureLoader));
|
||||||
router.post("/update", validatePathParams("projectPath"), createUpdateHandler(featureLoader));
|
router.post('/update', validatePathParams('projectPath'), createUpdateHandler(featureLoader));
|
||||||
router.post("/delete", validatePathParams("projectPath"), createDeleteHandler(featureLoader));
|
router.post('/delete', validatePathParams('projectPath'), createDeleteHandler(featureLoader));
|
||||||
router.post("/agent-output", createAgentOutputHandler(featureLoader));
|
router.post('/agent-output', createAgentOutputHandler(featureLoader));
|
||||||
router.post("/generate-title", createGenerateTitleHandler());
|
router.post('/generate-title', createGenerateTitleHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /agent-output endpoint - Get agent output for a feature
|
* POST /agent-output endpoint - Get agent output for a feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { FeatureLoader } from "../../../services/feature-loader.js";
|
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createAgentOutputHandler(featureLoader: FeatureLoader) {
|
export function createAgentOutputHandler(featureLoader: FeatureLoader) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -15,22 +15,17 @@ export function createAgentOutputHandler(featureLoader: FeatureLoader) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res
|
res.status(400).json({
|
||||||
.status(400)
|
success: false,
|
||||||
.json({
|
error: 'projectPath and featureId are required',
|
||||||
success: false,
|
});
|
||||||
error: "projectPath and featureId are required",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await featureLoader.getAgentOutput(
|
const content = await featureLoader.getAgentOutput(projectPath, featureId);
|
||||||
projectPath,
|
|
||||||
featureId
|
|
||||||
);
|
|
||||||
res.json({ success: true, content });
|
res.json({ success: true, content });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get agent output failed");
|
logError(error, 'Get agent output failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
* POST /create endpoint - Create a new feature
|
* POST /create endpoint - Create a new feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { FeatureLoader } from "../../../services/feature-loader.js";
|
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||||
import type { Feature } from "@automaker/types";
|
import type { Feature } from '@automaker/types';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createCreateHandler(featureLoader: FeatureLoader) {
|
export function createCreateHandler(featureLoader: FeatureLoader) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -18,7 +18,7 @@ export function createCreateHandler(featureLoader: FeatureLoader) {
|
|||||||
if (!projectPath || !feature) {
|
if (!projectPath || !feature) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath and feature are required",
|
error: 'projectPath and feature are required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -26,7 +26,7 @@ export function createCreateHandler(featureLoader: FeatureLoader) {
|
|||||||
const created = await featureLoader.create(projectPath, feature);
|
const created = await featureLoader.create(projectPath, feature);
|
||||||
res.json({ success: true, feature: created });
|
res.json({ success: true, feature: created });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Create feature failed");
|
logError(error, 'Create feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /delete endpoint - Delete a feature
|
* POST /delete endpoint - Delete a feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { FeatureLoader } from "../../../services/feature-loader.js";
|
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createDeleteHandler(featureLoader: FeatureLoader) {
|
export function createDeleteHandler(featureLoader: FeatureLoader) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -15,19 +15,17 @@ export function createDeleteHandler(featureLoader: FeatureLoader) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res
|
res.status(400).json({
|
||||||
.status(400)
|
success: false,
|
||||||
.json({
|
error: 'projectPath and featureId are required',
|
||||||
success: false,
|
});
|
||||||
error: "projectPath and featureId are required",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await featureLoader.delete(projectPath, featureId);
|
const success = await featureLoader.delete(projectPath, featureId);
|
||||||
res.json({ success });
|
res.json({ success });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Delete feature failed");
|
logError(error, 'Delete feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -4,12 +4,12 @@
|
|||||||
* Uses Claude Haiku to generate a short, descriptive title from feature description.
|
* Uses Claude Haiku to generate a short, descriptive title from feature description.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { CLAUDE_MODEL_MAP } from "@automaker/model-resolver";
|
import { CLAUDE_MODEL_MAP } from '@automaker/model-resolver';
|
||||||
|
|
||||||
const logger = createLogger("GenerateTitle");
|
const logger = createLogger('GenerateTitle');
|
||||||
|
|
||||||
interface GenerateTitleRequestBody {
|
interface GenerateTitleRequestBody {
|
||||||
description: string;
|
description: string;
|
||||||
@@ -44,16 +44,16 @@ async function extractTextFromStream(
|
|||||||
};
|
};
|
||||||
}>
|
}>
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
let responseText = "";
|
let responseText = '';
|
||||||
|
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
if (msg.type === "assistant" && msg.message?.content) {
|
if (msg.type === 'assistant' && msg.message?.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text" && block.text) {
|
if (block.type === 'text' && block.text) {
|
||||||
responseText += block.text;
|
responseText += block.text;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === "result" && msg.subtype === "success") {
|
} else if (msg.type === 'result' && msg.subtype === 'success') {
|
||||||
responseText = msg.result || responseText;
|
responseText = msg.result || responseText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -61,18 +61,15 @@ async function extractTextFromStream(
|
|||||||
return responseText;
|
return responseText;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createGenerateTitleHandler(): (
|
export function createGenerateTitleHandler(): (req: Request, res: Response) => Promise<void> {
|
||||||
req: Request,
|
|
||||||
res: Response
|
|
||||||
) => Promise<void> {
|
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const { description } = req.body as GenerateTitleRequestBody;
|
const { description } = req.body as GenerateTitleRequestBody;
|
||||||
|
|
||||||
if (!description || typeof description !== "string") {
|
if (!description || typeof description !== 'string') {
|
||||||
const response: GenerateTitleErrorResponse = {
|
const response: GenerateTitleErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "description is required and must be a string",
|
error: 'description is required and must be a string',
|
||||||
};
|
};
|
||||||
res.status(400).json(response);
|
res.status(400).json(response);
|
||||||
return;
|
return;
|
||||||
@@ -82,7 +79,7 @@ export function createGenerateTitleHandler(): (
|
|||||||
if (trimmedDescription.length === 0) {
|
if (trimmedDescription.length === 0) {
|
||||||
const response: GenerateTitleErrorResponse = {
|
const response: GenerateTitleErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "description cannot be empty",
|
error: 'description cannot be empty',
|
||||||
};
|
};
|
||||||
res.status(400).json(response);
|
res.status(400).json(response);
|
||||||
return;
|
return;
|
||||||
@@ -99,17 +96,17 @@ export function createGenerateTitleHandler(): (
|
|||||||
systemPrompt: SYSTEM_PROMPT,
|
systemPrompt: SYSTEM_PROMPT,
|
||||||
maxTurns: 1,
|
maxTurns: 1,
|
||||||
allowedTools: [],
|
allowedTools: [],
|
||||||
permissionMode: "acceptEdits",
|
permissionMode: 'acceptEdits',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const title = await extractTextFromStream(stream);
|
const title = await extractTextFromStream(stream);
|
||||||
|
|
||||||
if (!title || title.trim().length === 0) {
|
if (!title || title.trim().length === 0) {
|
||||||
logger.warn("Received empty response from Claude");
|
logger.warn('Received empty response from Claude');
|
||||||
const response: GenerateTitleErrorResponse = {
|
const response: GenerateTitleErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
error: "Failed to generate title - empty response",
|
error: 'Failed to generate title - empty response',
|
||||||
};
|
};
|
||||||
res.status(500).json(response);
|
res.status(500).json(response);
|
||||||
return;
|
return;
|
||||||
@@ -123,9 +120,8 @@ export function createGenerateTitleHandler(): (
|
|||||||
};
|
};
|
||||||
res.json(response);
|
res.json(response);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred';
|
||||||
error instanceof Error ? error.message : "Unknown error occurred";
|
logger.error('Title generation failed:', errorMessage);
|
||||||
logger.error("Title generation failed:", errorMessage);
|
|
||||||
|
|
||||||
const response: GenerateTitleErrorResponse = {
|
const response: GenerateTitleErrorResponse = {
|
||||||
success: false,
|
success: false,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /get endpoint - Get a single feature
|
* POST /get endpoint - Get a single feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { FeatureLoader } from "../../../services/feature-loader.js";
|
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createGetHandler(featureLoader: FeatureLoader) {
|
export function createGetHandler(featureLoader: FeatureLoader) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -15,24 +15,22 @@ export function createGetHandler(featureLoader: FeatureLoader) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res
|
res.status(400).json({
|
||||||
.status(400)
|
success: false,
|
||||||
.json({
|
error: 'projectPath and featureId are required',
|
||||||
success: false,
|
});
|
||||||
error: "projectPath and featureId are required",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const feature = await featureLoader.get(projectPath, featureId);
|
const feature = await featureLoader.get(projectPath, featureId);
|
||||||
if (!feature) {
|
if (!feature) {
|
||||||
res.status(404).json({ success: false, error: "Feature not found" });
|
res.status(404).json({ success: false, error: 'Feature not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, feature });
|
res.json({ success: true, feature });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get feature failed");
|
logError(error, 'Get feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /list endpoint - List all features for a project
|
* POST /list endpoint - List all features for a project
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { FeatureLoader } from "../../../services/feature-loader.js";
|
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createListHandler(featureLoader: FeatureLoader) {
|
export function createListHandler(featureLoader: FeatureLoader) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -12,16 +12,14 @@ export function createListHandler(featureLoader: FeatureLoader) {
|
|||||||
const { projectPath } = req.body as { projectPath: string };
|
const { projectPath } = req.body as { projectPath: string };
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'projectPath is required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "projectPath is required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const features = await featureLoader.getAll(projectPath);
|
const features = await featureLoader.getAll(projectPath);
|
||||||
res.json({ success: true, features });
|
res.json({ success: true, features });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "List features failed");
|
logError(error, 'List features failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
* POST /update endpoint - Update a feature
|
* POST /update endpoint - Update a feature
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { FeatureLoader } from "../../../services/feature-loader.js";
|
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||||
import type { Feature } from "@automaker/types";
|
import type { Feature } from '@automaker/types';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createUpdateHandler(featureLoader: FeatureLoader) {
|
export function createUpdateHandler(featureLoader: FeatureLoader) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -19,19 +19,15 @@ export function createUpdateHandler(featureLoader: FeatureLoader) {
|
|||||||
if (!projectPath || !featureId || !updates) {
|
if (!projectPath || !featureId || !updates) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath, featureId, and updates are required",
|
error: 'projectPath, featureId, and updates are required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updated = await featureLoader.update(
|
const updated = await featureLoader.update(projectPath, featureId, updates);
|
||||||
projectPath,
|
|
||||||
featureId,
|
|
||||||
updates
|
|
||||||
);
|
|
||||||
res.json({ success: true, feature: updated });
|
res.json({ success: true, feature: updated });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Update feature failed");
|
logError(error, 'Update feature failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
* Common utilities for fs routes
|
* Common utilities for fs routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("FS");
|
const logger = createLogger('FS');
|
||||||
|
|
||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -3,40 +3,40 @@
|
|||||||
* Provides REST API equivalents for Electron IPC file operations
|
* Provides REST API equivalents for Electron IPC file operations
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { createReadHandler } from "./routes/read.js";
|
import { createReadHandler } from './routes/read.js';
|
||||||
import { createWriteHandler } from "./routes/write.js";
|
import { createWriteHandler } from './routes/write.js';
|
||||||
import { createMkdirHandler } from "./routes/mkdir.js";
|
import { createMkdirHandler } from './routes/mkdir.js';
|
||||||
import { createReaddirHandler } from "./routes/readdir.js";
|
import { createReaddirHandler } from './routes/readdir.js';
|
||||||
import { createExistsHandler } from "./routes/exists.js";
|
import { createExistsHandler } from './routes/exists.js';
|
||||||
import { createStatHandler } from "./routes/stat.js";
|
import { createStatHandler } from './routes/stat.js';
|
||||||
import { createDeleteHandler } from "./routes/delete.js";
|
import { createDeleteHandler } from './routes/delete.js';
|
||||||
import { createValidatePathHandler } from "./routes/validate-path.js";
|
import { createValidatePathHandler } from './routes/validate-path.js';
|
||||||
import { createResolveDirectoryHandler } from "./routes/resolve-directory.js";
|
import { createResolveDirectoryHandler } from './routes/resolve-directory.js';
|
||||||
import { createSaveImageHandler } from "./routes/save-image.js";
|
import { createSaveImageHandler } from './routes/save-image.js';
|
||||||
import { createBrowseHandler } from "./routes/browse.js";
|
import { createBrowseHandler } from './routes/browse.js';
|
||||||
import { createImageHandler } from "./routes/image.js";
|
import { createImageHandler } from './routes/image.js';
|
||||||
import { createSaveBoardBackgroundHandler } from "./routes/save-board-background.js";
|
import { createSaveBoardBackgroundHandler } from './routes/save-board-background.js';
|
||||||
import { createDeleteBoardBackgroundHandler } from "./routes/delete-board-background.js";
|
import { createDeleteBoardBackgroundHandler } from './routes/delete-board-background.js';
|
||||||
|
|
||||||
export function createFsRoutes(_events: EventEmitter): Router {
|
export function createFsRoutes(_events: EventEmitter): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/read", createReadHandler());
|
router.post('/read', createReadHandler());
|
||||||
router.post("/write", createWriteHandler());
|
router.post('/write', createWriteHandler());
|
||||||
router.post("/mkdir", createMkdirHandler());
|
router.post('/mkdir', createMkdirHandler());
|
||||||
router.post("/readdir", createReaddirHandler());
|
router.post('/readdir', createReaddirHandler());
|
||||||
router.post("/exists", createExistsHandler());
|
router.post('/exists', createExistsHandler());
|
||||||
router.post("/stat", createStatHandler());
|
router.post('/stat', createStatHandler());
|
||||||
router.post("/delete", createDeleteHandler());
|
router.post('/delete', createDeleteHandler());
|
||||||
router.post("/validate-path", createValidatePathHandler());
|
router.post('/validate-path', createValidatePathHandler());
|
||||||
router.post("/resolve-directory", createResolveDirectoryHandler());
|
router.post('/resolve-directory', createResolveDirectoryHandler());
|
||||||
router.post("/save-image", createSaveImageHandler());
|
router.post('/save-image', createSaveImageHandler());
|
||||||
router.post("/browse", createBrowseHandler());
|
router.post('/browse', createBrowseHandler());
|
||||||
router.get("/image", createImageHandler());
|
router.get('/image', createImageHandler());
|
||||||
router.post("/save-board-background", createSaveBoardBackgroundHandler());
|
router.post('/save-board-background', createSaveBoardBackgroundHandler());
|
||||||
router.post("/delete-board-background", createDeleteBoardBackgroundHandler());
|
router.post('/delete-board-background', createDeleteBoardBackgroundHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
* Common utilities for git routes
|
* Common utilities for git routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("Git");
|
const logger = createLogger('Git');
|
||||||
|
|
||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
* Git routes - HTTP API for git operations (non-worktree)
|
* Git routes - HTTP API for git operations (non-worktree)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import { validatePathParams } from "../../middleware/validate-paths.js";
|
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||||
import { createDiffsHandler } from "./routes/diffs.js";
|
import { createDiffsHandler } from './routes/diffs.js';
|
||||||
import { createFileDiffHandler } from "./routes/file-diff.js";
|
import { createFileDiffHandler } from './routes/file-diff.js';
|
||||||
|
|
||||||
export function createGitRoutes(): Router {
|
export function createGitRoutes(): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.post("/diffs", validatePathParams("projectPath"), createDiffsHandler());
|
router.post('/diffs', validatePathParams('projectPath'), createDiffsHandler());
|
||||||
router.post("/file-diff", validatePathParams("projectPath", "filePath"), createFileDiffHandler());
|
router.post('/file-diff', validatePathParams('projectPath', 'filePath'), createFileDiffHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /diffs endpoint - Get diffs for the main project
|
* POST /diffs endpoint - Get diffs for the main project
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getGitRepositoryDiffs } from "../../common.js";
|
import { getGitRepositoryDiffs } from '../../common.js';
|
||||||
|
|
||||||
export function createDiffsHandler() {
|
export function createDiffsHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -12,7 +12,7 @@ export function createDiffsHandler() {
|
|||||||
const { projectPath } = req.body as { projectPath: string };
|
const { projectPath } = req.body as { projectPath: string };
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
res.status(400).json({ success: false, error: "projectPath required" });
|
res.status(400).json({ success: false, error: 'projectPath required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,11 +25,11 @@ export function createDiffsHandler() {
|
|||||||
hasChanges: result.hasChanges,
|
hasChanges: result.hasChanges,
|
||||||
});
|
});
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
logError(innerError, "Git diff failed");
|
logError(innerError, 'Git diff failed');
|
||||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
res.json({ success: true, diff: '', files: [], hasChanges: false });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get diffs failed");
|
logError(error, 'Get diffs failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* POST /file-diff endpoint - Get diff for a specific file
|
* POST /file-diff endpoint - Get diff for a specific file
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { generateSyntheticDiffForNewFile } from "../../common.js";
|
import { generateSyntheticDiffForNewFile } from '../../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -19,20 +19,17 @@ export function createFileDiffHandler() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !filePath) {
|
if (!projectPath || !filePath) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'projectPath and filePath required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "projectPath and filePath required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First check if the file is untracked
|
// First check if the file is untracked
|
||||||
const { stdout: status } = await execAsync(
|
const { stdout: status } = await execAsync(`git status --porcelain -- "${filePath}"`, {
|
||||||
`git status --porcelain -- "${filePath}"`,
|
cwd: projectPath,
|
||||||
{ cwd: projectPath }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const isUntracked = status.trim().startsWith("??");
|
const isUntracked = status.trim().startsWith('??');
|
||||||
|
|
||||||
let diff: string;
|
let diff: string;
|
||||||
if (isUntracked) {
|
if (isUntracked) {
|
||||||
@@ -40,23 +37,20 @@ export function createFileDiffHandler() {
|
|||||||
diff = await generateSyntheticDiffForNewFile(projectPath, filePath);
|
diff = await generateSyntheticDiffForNewFile(projectPath, filePath);
|
||||||
} else {
|
} else {
|
||||||
// Use regular git diff for tracked files
|
// Use regular git diff for tracked files
|
||||||
const result = await execAsync(
|
const result = await execAsync(`git diff HEAD -- "${filePath}"`, {
|
||||||
`git diff HEAD -- "${filePath}"`,
|
cwd: projectPath,
|
||||||
{
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
cwd: projectPath,
|
});
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
diff = result.stdout;
|
diff = result.stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, diff, filePath });
|
res.json({ success: true, diff, filePath });
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
logError(innerError, "Git file diff failed");
|
logError(innerError, 'Git file diff failed');
|
||||||
res.json({ success: true, diff: "", filePath });
|
res.json({ success: true, diff: '', filePath });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get file diff failed");
|
logError(error, 'Get file diff failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
* Common utilities for health routes
|
* Common utilities for health routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("Health");
|
const logger = createLogger('Health');
|
||||||
|
|
||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
* Health check routes
|
* Health check routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import { createIndexHandler } from "./routes/index.js";
|
import { createIndexHandler } from './routes/index.js';
|
||||||
import { createDetailedHandler } from "./routes/detailed.js";
|
import { createDetailedHandler } from './routes/detailed.js';
|
||||||
|
|
||||||
export function createHealthRoutes(): Router {
|
export function createHealthRoutes(): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/", createIndexHandler());
|
router.get('/', createIndexHandler());
|
||||||
router.get("/detailed", createDetailedHandler());
|
router.get('/detailed', createDetailedHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
* GET /detailed endpoint - Detailed health check
|
* GET /detailed endpoint - Detailed health check
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { getAuthStatus } from "../../../lib/auth.js";
|
import { getAuthStatus } from '../../../lib/auth.js';
|
||||||
|
|
||||||
export function createDetailedHandler() {
|
export function createDetailedHandler() {
|
||||||
return (_req: Request, res: Response): void => {
|
return (_req: Request, res: Response): void => {
|
||||||
res.json({
|
res.json({
|
||||||
status: "ok",
|
status: 'ok',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
version: process.env.npm_package_version || "0.1.0",
|
version: process.env.npm_package_version || '0.1.0',
|
||||||
uptime: process.uptime(),
|
uptime: process.uptime(),
|
||||||
memory: process.memoryUsage(),
|
memory: process.memoryUsage(),
|
||||||
dataDir: process.env.DATA_DIR || "./data",
|
dataDir: process.env.DATA_DIR || './data',
|
||||||
auth: getAuthStatus(),
|
auth: getAuthStatus(),
|
||||||
env: {
|
env: {
|
||||||
nodeVersion: process.version,
|
nodeVersion: process.version,
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
* GET / endpoint - Basic health check
|
* GET / endpoint - Basic health check
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
|
|
||||||
export function createIndexHandler() {
|
export function createIndexHandler() {
|
||||||
return (_req: Request, res: Response): void => {
|
return (_req: Request, res: Response): void => {
|
||||||
res.json({
|
res.json({
|
||||||
status: "ok",
|
status: 'ok',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
version: process.env.npm_package_version || "0.1.0",
|
version: process.env.npm_package_version || '0.1.0',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
* Common utilities for models routes
|
* Common utilities for models routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("Models");
|
const logger = createLogger('Models');
|
||||||
|
|
||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
* Models routes - HTTP API for model providers and availability
|
* Models routes - HTTP API for model providers and availability
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import { createAvailableHandler } from "./routes/available.js";
|
import { createAvailableHandler } from './routes/available.js';
|
||||||
import { createProvidersHandler } from "./routes/providers.js";
|
import { createProvidersHandler } from './routes/providers.js';
|
||||||
|
|
||||||
export function createModelsRoutes(): Router {
|
export function createModelsRoutes(): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/available", createAvailableHandler());
|
router.get('/available', createAvailableHandler());
|
||||||
router.get("/providers", createProvidersHandler());
|
router.get('/providers', createProvidersHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
* GET /available endpoint - Get available models
|
* GET /available endpoint - Get available models
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
interface ModelDefinition {
|
interface ModelDefinition {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -20,36 +20,36 @@ export function createAvailableHandler() {
|
|||||||
try {
|
try {
|
||||||
const models: ModelDefinition[] = [
|
const models: ModelDefinition[] = [
|
||||||
{
|
{
|
||||||
id: "claude-opus-4-5-20251101",
|
id: 'claude-opus-4-5-20251101',
|
||||||
name: "Claude Opus 4.5",
|
name: 'Claude Opus 4.5',
|
||||||
provider: "anthropic",
|
provider: 'anthropic',
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxOutputTokens: 16384,
|
maxOutputTokens: 16384,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "claude-sonnet-4-20250514",
|
id: 'claude-sonnet-4-20250514',
|
||||||
name: "Claude Sonnet 4",
|
name: 'Claude Sonnet 4',
|
||||||
provider: "anthropic",
|
provider: 'anthropic',
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxOutputTokens: 16384,
|
maxOutputTokens: 16384,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "claude-3-5-sonnet-20241022",
|
id: 'claude-3-5-sonnet-20241022',
|
||||||
name: "Claude 3.5 Sonnet",
|
name: 'Claude 3.5 Sonnet',
|
||||||
provider: "anthropic",
|
provider: 'anthropic',
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxOutputTokens: 8192,
|
maxOutputTokens: 8192,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
supportsTools: true,
|
supportsTools: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "claude-3-5-haiku-20241022",
|
id: 'claude-3-5-haiku-20241022',
|
||||||
name: "Claude 3.5 Haiku",
|
name: 'Claude 3.5 Haiku',
|
||||||
provider: "anthropic",
|
provider: 'anthropic',
|
||||||
contextWindow: 200000,
|
contextWindow: 200000,
|
||||||
maxOutputTokens: 8192,
|
maxOutputTokens: 8192,
|
||||||
supportsVision: true,
|
supportsVision: true,
|
||||||
@@ -59,7 +59,7 @@ export function createAvailableHandler() {
|
|||||||
|
|
||||||
res.json({ success: true, models });
|
res.json({ success: true, models });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get available models failed");
|
logError(error, 'Get available models failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* GET /providers endpoint - Check provider status
|
* GET /providers endpoint - Check provider status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { ProviderFactory } from "../../../providers/provider-factory.js";
|
import { ProviderFactory } from '../../../providers/provider-factory.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createProvidersHandler() {
|
export function createProvidersHandler() {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
@@ -21,7 +21,7 @@ export function createProvidersHandler() {
|
|||||||
|
|
||||||
res.json({ success: true, providers });
|
res.json({ success: true, providers });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get providers failed");
|
logError(error, 'Get providers failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
* Common utilities for running-agents routes
|
* Common utilities for running-agents routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("RunningAgents");
|
const logger = createLogger('RunningAgents');
|
||||||
|
|
||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -2,16 +2,14 @@
|
|||||||
* Running Agents routes - HTTP API for tracking active agent executions
|
* Running Agents routes - HTTP API for tracking active agent executions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import type { AutoModeService } from "../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../services/auto-mode-service.js';
|
||||||
import { createIndexHandler } from "./routes/index.js";
|
import { createIndexHandler } from './routes/index.js';
|
||||||
|
|
||||||
export function createRunningAgentsRoutes(
|
export function createRunningAgentsRoutes(autoModeService: AutoModeService): Router {
|
||||||
autoModeService: AutoModeService
|
|
||||||
): Router {
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/", createIndexHandler(autoModeService));
|
router.get('/', createIndexHandler(autoModeService));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* GET / endpoint - Get all running agents
|
* GET / endpoint - Get all running agents
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createIndexHandler(autoModeService: AutoModeService) {
|
export function createIndexHandler(autoModeService: AutoModeService) {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
@@ -18,7 +18,7 @@ export function createIndexHandler(autoModeService: AutoModeService) {
|
|||||||
totalCount: runningAgents.length,
|
totalCount: runningAgents.length,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get running agents failed");
|
logError(error, 'Get running agents failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,10 @@
|
|||||||
* Common utilities for sessions routes
|
* Common utilities for sessions routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("Sessions");
|
const logger = createLogger('Sessions');
|
||||||
|
|
||||||
// Re-export shared utilities
|
// Re-export shared utilities
|
||||||
export { getErrorMessageShared as getErrorMessage };
|
export { getErrorMessageShared as getErrorMessage };
|
||||||
|
|||||||
@@ -2,24 +2,24 @@
|
|||||||
* Sessions routes - HTTP API for session management
|
* Sessions routes - HTTP API for session management
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import { AgentService } from "../../services/agent-service.js";
|
import { AgentService } from '../../services/agent-service.js';
|
||||||
import { createIndexHandler } from "./routes/index.js";
|
import { createIndexHandler } from './routes/index.js';
|
||||||
import { createCreateHandler } from "./routes/create.js";
|
import { createCreateHandler } from './routes/create.js';
|
||||||
import { createUpdateHandler } from "./routes/update.js";
|
import { createUpdateHandler } from './routes/update.js';
|
||||||
import { createArchiveHandler } from "./routes/archive.js";
|
import { createArchiveHandler } from './routes/archive.js';
|
||||||
import { createUnarchiveHandler } from "./routes/unarchive.js";
|
import { createUnarchiveHandler } from './routes/unarchive.js';
|
||||||
import { createDeleteHandler } from "./routes/delete.js";
|
import { createDeleteHandler } from './routes/delete.js';
|
||||||
|
|
||||||
export function createSessionsRoutes(agentService: AgentService): Router {
|
export function createSessionsRoutes(agentService: AgentService): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/", createIndexHandler(agentService));
|
router.get('/', createIndexHandler(agentService));
|
||||||
router.post("/", createCreateHandler(agentService));
|
router.post('/', createCreateHandler(agentService));
|
||||||
router.put("/:sessionId", createUpdateHandler(agentService));
|
router.put('/:sessionId', createUpdateHandler(agentService));
|
||||||
router.post("/:sessionId/archive", createArchiveHandler(agentService));
|
router.post('/:sessionId/archive', createArchiveHandler(agentService));
|
||||||
router.post("/:sessionId/unarchive", createUnarchiveHandler(agentService));
|
router.post('/:sessionId/unarchive', createUnarchiveHandler(agentService));
|
||||||
router.delete("/:sessionId", createDeleteHandler(agentService));
|
router.delete('/:sessionId', createDeleteHandler(agentService));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /:sessionId/archive endpoint - Archive a session
|
* POST /:sessionId/archive endpoint - Archive a session
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createArchiveHandler(agentService: AgentService) {
|
export function createArchiveHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -13,13 +13,13 @@ export function createArchiveHandler(agentService: AgentService) {
|
|||||||
const success = await agentService.archiveSession(sessionId);
|
const success = await agentService.archiveSession(sessionId);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
res.status(404).json({ success: false, error: "Session not found" });
|
res.status(404).json({ success: false, error: 'Session not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Archive session failed");
|
logError(error, 'Archive session failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST / endpoint - Create a new session
|
* POST / endpoint - Create a new session
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createCreateHandler(agentService: AgentService) {
|
export function createCreateHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -17,19 +17,14 @@ export function createCreateHandler(agentService: AgentService) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!name) {
|
if (!name) {
|
||||||
res.status(400).json({ success: false, error: "name is required" });
|
res.status(400).json({ success: false, error: 'name is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await agentService.createSession(
|
const session = await agentService.createSession(name, projectPath, workingDirectory, model);
|
||||||
name,
|
|
||||||
projectPath,
|
|
||||||
workingDirectory,
|
|
||||||
model
|
|
||||||
);
|
|
||||||
res.json({ success: true, session });
|
res.json({ success: true, session });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Create session failed");
|
logError(error, 'Create session failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* DELETE /:sessionId endpoint - Delete a session
|
* DELETE /:sessionId endpoint - Delete a session
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createDeleteHandler(agentService: AgentService) {
|
export function createDeleteHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -13,13 +13,13 @@ export function createDeleteHandler(agentService: AgentService) {
|
|||||||
const success = await agentService.deleteSession(sessionId);
|
const success = await agentService.deleteSession(sessionId);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
res.status(404).json({ success: false, error: "Session not found" });
|
res.status(404).json({ success: false, error: 'Session not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Delete session failed");
|
logError(error, 'Delete session failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
* GET / endpoint - List all sessions
|
* GET / endpoint - List all sessions
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createIndexHandler(agentService: AgentService) {
|
export function createIndexHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const includeArchived = req.query.includeArchived === "true";
|
const includeArchived = req.query.includeArchived === 'true';
|
||||||
const sessionsRaw = await agentService.listSessions(includeArchived);
|
const sessionsRaw = await agentService.listSessions(includeArchived);
|
||||||
|
|
||||||
// Transform to match frontend SessionListItem interface
|
// Transform to match frontend SessionListItem interface
|
||||||
@@ -17,7 +17,7 @@ export function createIndexHandler(agentService: AgentService) {
|
|||||||
sessionsRaw.map(async (s) => {
|
sessionsRaw.map(async (s) => {
|
||||||
const messages = await agentService.loadSession(s.id);
|
const messages = await agentService.loadSession(s.id);
|
||||||
const lastMessage = messages[messages.length - 1];
|
const lastMessage = messages[messages.length - 1];
|
||||||
const preview = lastMessage?.content?.slice(0, 100) || "";
|
const preview = lastMessage?.content?.slice(0, 100) || '';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: s.id,
|
id: s.id,
|
||||||
@@ -36,7 +36,7 @@ export function createIndexHandler(agentService: AgentService) {
|
|||||||
|
|
||||||
res.json({ success: true, sessions });
|
res.json({ success: true, sessions });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "List sessions failed");
|
logError(error, 'List sessions failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* POST /:sessionId/unarchive endpoint - Unarchive a session
|
* POST /:sessionId/unarchive endpoint - Unarchive a session
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createUnarchiveHandler(agentService: AgentService) {
|
export function createUnarchiveHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -13,13 +13,13 @@ export function createUnarchiveHandler(agentService: AgentService) {
|
|||||||
const success = await agentService.unarchiveSession(sessionId);
|
const success = await agentService.unarchiveSession(sessionId);
|
||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
res.status(404).json({ success: false, error: "Session not found" });
|
res.status(404).json({ success: false, error: 'Session not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Unarchive session failed");
|
logError(error, 'Unarchive session failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
* PUT /:sessionId endpoint - Update a session
|
* PUT /:sessionId endpoint - Update a session
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { AgentService } from "../../../services/agent-service.js";
|
import { AgentService } from '../../../services/agent-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createUpdateHandler(agentService: AgentService) {
|
export function createUpdateHandler(agentService: AgentService) {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -22,13 +22,13 @@ export function createUpdateHandler(agentService: AgentService) {
|
|||||||
model,
|
model,
|
||||||
});
|
});
|
||||||
if (!session) {
|
if (!session) {
|
||||||
res.status(404).json({ success: false, error: "Session not found" });
|
res.status(404).json({ success: false, error: 'Session not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, session });
|
res.json({ success: true, session });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Update session failed");
|
logError(error, 'Update session failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,14 +5,11 @@
|
|||||||
* Re-exports error handling helpers from the parent routes module.
|
* Re-exports error handling helpers from the parent routes module.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
/** Logger instance for settings-related operations */
|
/** Logger instance for settings-related operations */
|
||||||
export const logger = createLogger("Settings");
|
export const logger = createLogger('Settings');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract user-friendly error message from error objects
|
* Extract user-friendly error message from error objects
|
||||||
|
|||||||
@@ -12,17 +12,17 @@
|
|||||||
* Mounted at /api/settings in the main server.
|
* Mounted at /api/settings in the main server.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import type { SettingsService } from "../../services/settings-service.js";
|
import type { SettingsService } from '../../services/settings-service.js';
|
||||||
import { validatePathParams } from "../../middleware/validate-paths.js";
|
import { validatePathParams } from '../../middleware/validate-paths.js';
|
||||||
import { createGetGlobalHandler } from "./routes/get-global.js";
|
import { createGetGlobalHandler } from './routes/get-global.js';
|
||||||
import { createUpdateGlobalHandler } from "./routes/update-global.js";
|
import { createUpdateGlobalHandler } from './routes/update-global.js';
|
||||||
import { createGetCredentialsHandler } from "./routes/get-credentials.js";
|
import { createGetCredentialsHandler } from './routes/get-credentials.js';
|
||||||
import { createUpdateCredentialsHandler } from "./routes/update-credentials.js";
|
import { createUpdateCredentialsHandler } from './routes/update-credentials.js';
|
||||||
import { createGetProjectHandler } from "./routes/get-project.js";
|
import { createGetProjectHandler } from './routes/get-project.js';
|
||||||
import { createUpdateProjectHandler } from "./routes/update-project.js";
|
import { createUpdateProjectHandler } from './routes/update-project.js';
|
||||||
import { createMigrateHandler } from "./routes/migrate.js";
|
import { createMigrateHandler } from './routes/migrate.js';
|
||||||
import { createStatusHandler } from "./routes/status.js";
|
import { createStatusHandler } from './routes/status.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create settings router with all endpoints
|
* Create settings router with all endpoints
|
||||||
@@ -47,22 +47,30 @@ export function createSettingsRoutes(settingsService: SettingsService): Router {
|
|||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
// Status endpoint (check if migration needed)
|
// Status endpoint (check if migration needed)
|
||||||
router.get("/status", createStatusHandler(settingsService));
|
router.get('/status', createStatusHandler(settingsService));
|
||||||
|
|
||||||
// Global settings
|
// Global settings
|
||||||
router.get("/global", createGetGlobalHandler(settingsService));
|
router.get('/global', createGetGlobalHandler(settingsService));
|
||||||
router.put("/global", createUpdateGlobalHandler(settingsService));
|
router.put('/global', createUpdateGlobalHandler(settingsService));
|
||||||
|
|
||||||
// Credentials (separate for security)
|
// Credentials (separate for security)
|
||||||
router.get("/credentials", createGetCredentialsHandler(settingsService));
|
router.get('/credentials', createGetCredentialsHandler(settingsService));
|
||||||
router.put("/credentials", createUpdateCredentialsHandler(settingsService));
|
router.put('/credentials', createUpdateCredentialsHandler(settingsService));
|
||||||
|
|
||||||
// Project settings
|
// Project settings
|
||||||
router.post("/project", validatePathParams("projectPath"), createGetProjectHandler(settingsService));
|
router.post(
|
||||||
router.put("/project", validatePathParams("projectPath"), createUpdateProjectHandler(settingsService));
|
'/project',
|
||||||
|
validatePathParams('projectPath'),
|
||||||
|
createGetProjectHandler(settingsService)
|
||||||
|
);
|
||||||
|
router.put(
|
||||||
|
'/project',
|
||||||
|
validatePathParams('projectPath'),
|
||||||
|
createUpdateProjectHandler(settingsService)
|
||||||
|
);
|
||||||
|
|
||||||
// Migration from localStorage
|
// Migration from localStorage
|
||||||
router.post("/migrate", createMigrateHandler(settingsService));
|
router.post('/migrate', createMigrateHandler(settingsService));
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
* Response: `{ "success": true, "credentials": { anthropic } }`
|
* Response: `{ "success": true, "credentials": { anthropic } }`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { SettingsService } from "../../../services/settings-service.js";
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create handler factory for GET /api/settings/credentials
|
* Create handler factory for GET /api/settings/credentials
|
||||||
@@ -28,7 +28,7 @@ export function createGetCredentialsHandler(settingsService: SettingsService) {
|
|||||||
credentials,
|
credentials,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get credentials failed");
|
logError(error, 'Get credentials failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
* Response: `{ "success": true, "settings": GlobalSettings }`
|
* Response: `{ "success": true, "settings": GlobalSettings }`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { SettingsService } from "../../../services/settings-service.js";
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create handler factory for GET /api/settings/global
|
* Create handler factory for GET /api/settings/global
|
||||||
@@ -27,7 +27,7 @@ export function createGetGlobalHandler(settingsService: SettingsService) {
|
|||||||
settings,
|
settings,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get global settings failed");
|
logError(error, 'Get global settings failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,9 @@
|
|||||||
* Response: `{ "success": true, "settings": ProjectSettings }`
|
* Response: `{ "success": true, "settings": ProjectSettings }`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { SettingsService } from "../../../services/settings-service.js";
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create handler factory for POST /api/settings/project
|
* Create handler factory for POST /api/settings/project
|
||||||
@@ -23,10 +23,10 @@ export function createGetProjectHandler(settingsService: SettingsService) {
|
|||||||
try {
|
try {
|
||||||
const { projectPath } = req.body as { projectPath?: string };
|
const { projectPath } = req.body as { projectPath?: string };
|
||||||
|
|
||||||
if (!projectPath || typeof projectPath !== "string") {
|
if (!projectPath || typeof projectPath !== 'string') {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath is required",
|
error: 'projectPath is required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ export function createGetProjectHandler(settingsService: SettingsService) {
|
|||||||
settings,
|
settings,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get project settings failed");
|
logError(error, 'Get project settings failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { SettingsService } from "../../../services/settings-service.js";
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import { getErrorMessage, logError, logger } from "../common.js";
|
import { getErrorMessage, logError, logger } from '../common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create handler factory for POST /api/settings/migrate
|
* Create handler factory for POST /api/settings/migrate
|
||||||
@@ -45,32 +45,30 @@ export function createMigrateHandler(settingsService: SettingsService) {
|
|||||||
try {
|
try {
|
||||||
const { data } = req.body as {
|
const { data } = req.body as {
|
||||||
data?: {
|
data?: {
|
||||||
"automaker-storage"?: string;
|
'automaker-storage'?: string;
|
||||||
"automaker-setup"?: string;
|
'automaker-setup'?: string;
|
||||||
"worktree-panel-collapsed"?: string;
|
'worktree-panel-collapsed'?: string;
|
||||||
"file-browser-recent-folders"?: string;
|
'file-browser-recent-folders'?: string;
|
||||||
"automaker:lastProjectDir"?: string;
|
'automaker:lastProjectDir'?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!data || typeof data !== "object") {
|
if (!data || typeof data !== 'object') {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "data object is required containing localStorage data",
|
error: 'data object is required containing localStorage data',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Starting settings migration from localStorage");
|
logger.info('Starting settings migration from localStorage');
|
||||||
|
|
||||||
const result = await settingsService.migrateFromLocalStorage(data);
|
const result = await settingsService.migrateFromLocalStorage(data);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
logger.info(
|
logger.info(`Migration successful: ${result.migratedProjectCount} projects migrated`);
|
||||||
`Migration successful: ${result.migratedProjectCount} projects migrated`
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
logger.warn(`Migration completed with errors: ${result.errors.join(", ")}`);
|
logger.warn(`Migration completed with errors: ${result.errors.join(', ')}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
@@ -81,7 +79,7 @@ export function createMigrateHandler(settingsService: SettingsService) {
|
|||||||
errors: result.errors,
|
errors: result.errors,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Migration failed");
|
logError(error, 'Migration failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,9 +16,9 @@
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { SettingsService } from "../../../services/settings-service.js";
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create handler factory for GET /api/settings/status
|
* Create handler factory for GET /api/settings/status
|
||||||
@@ -40,7 +40,7 @@ export function createStatusHandler(settingsService: SettingsService) {
|
|||||||
needsMigration: !hasGlobalSettings,
|
needsMigration: !hasGlobalSettings,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get settings status failed");
|
logError(error, 'Get settings status failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
* Response: `{ "success": true, "credentials": { anthropic } }`
|
* Response: `{ "success": true, "credentials": { anthropic } }`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { SettingsService } from "../../../services/settings-service.js";
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import type { Credentials } from "../../../types/settings.js";
|
import type { Credentials } from '../../../types/settings.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create handler factory for PUT /api/settings/credentials
|
* Create handler factory for PUT /api/settings/credentials
|
||||||
@@ -19,17 +19,15 @@ import { getErrorMessage, logError } from "../common.js";
|
|||||||
* @param settingsService - Instance of SettingsService for file I/O
|
* @param settingsService - Instance of SettingsService for file I/O
|
||||||
* @returns Express request handler
|
* @returns Express request handler
|
||||||
*/
|
*/
|
||||||
export function createUpdateCredentialsHandler(
|
export function createUpdateCredentialsHandler(settingsService: SettingsService) {
|
||||||
settingsService: SettingsService
|
|
||||||
) {
|
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const updates = req.body as Partial<Credentials>;
|
const updates = req.body as Partial<Credentials>;
|
||||||
|
|
||||||
if (!updates || typeof updates !== "object") {
|
if (!updates || typeof updates !== 'object') {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid request body - expected credentials object",
|
error: 'Invalid request body - expected credentials object',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -44,7 +42,7 @@ export function createUpdateCredentialsHandler(
|
|||||||
credentials: masked,
|
credentials: masked,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Update credentials failed");
|
logError(error, 'Update credentials failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
* Response: `{ "success": true, "settings": GlobalSettings }`
|
* Response: `{ "success": true, "settings": GlobalSettings }`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { SettingsService } from "../../../services/settings-service.js";
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import type { GlobalSettings } from "../../../types/settings.js";
|
import type { GlobalSettings } from '../../../types/settings.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create handler factory for PUT /api/settings/global
|
* Create handler factory for PUT /api/settings/global
|
||||||
@@ -24,10 +24,10 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
|
|||||||
try {
|
try {
|
||||||
const updates = req.body as Partial<GlobalSettings>;
|
const updates = req.body as Partial<GlobalSettings>;
|
||||||
|
|
||||||
if (!updates || typeof updates !== "object") {
|
if (!updates || typeof updates !== 'object') {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid request body - expected settings object",
|
error: 'Invalid request body - expected settings object',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
|
|||||||
settings,
|
settings,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Update global settings failed");
|
logError(error, 'Update global settings failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,10 +8,10 @@
|
|||||||
* Response: `{ "success": true, "settings": ProjectSettings }`
|
* Response: `{ "success": true, "settings": ProjectSettings }`
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import type { SettingsService } from "../../../services/settings-service.js";
|
import type { SettingsService } from '../../../services/settings-service.js';
|
||||||
import type { ProjectSettings } from "../../../types/settings.js";
|
import type { ProjectSettings } from '../../../types/settings.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create handler factory for PUT /api/settings/project
|
* Create handler factory for PUT /api/settings/project
|
||||||
@@ -27,33 +27,30 @@ export function createUpdateProjectHandler(settingsService: SettingsService) {
|
|||||||
updates?: Partial<ProjectSettings>;
|
updates?: Partial<ProjectSettings>;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || typeof projectPath !== "string") {
|
if (!projectPath || typeof projectPath !== 'string') {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath is required",
|
error: 'projectPath is required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!updates || typeof updates !== "object") {
|
if (!updates || typeof updates !== 'object') {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "updates object is required",
|
error: 'updates object is required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = await settingsService.updateProjectSettings(
|
const settings = await settingsService.updateProjectSettings(projectPath, updates);
|
||||||
projectPath,
|
|
||||||
updates
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
settings,
|
settings,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Update project settings failed");
|
logError(error, 'Update project settings failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,15 +2,12 @@
|
|||||||
* Common utilities and state for setup routes
|
* Common utilities and state for setup routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import fs from 'fs/promises';
|
||||||
import {
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
|
|
||||||
const logger = createLogger("Setup");
|
const logger = createLogger('Setup');
|
||||||
|
|
||||||
// Storage for API keys (in-memory cache) - private
|
// Storage for API keys (in-memory cache) - private
|
||||||
const apiKeys: Record<string, string> = {};
|
const apiKeys: Record<string, string> = {};
|
||||||
@@ -39,22 +36,19 @@ export function getAllApiKeys(): Record<string, string> {
|
|||||||
/**
|
/**
|
||||||
* Helper to persist API keys to .env file
|
* Helper to persist API keys to .env file
|
||||||
*/
|
*/
|
||||||
export async function persistApiKeyToEnv(
|
export async function persistApiKeyToEnv(key: string, value: string): Promise<void> {
|
||||||
key: string,
|
const envPath = path.join(process.cwd(), '.env');
|
||||||
value: string
|
|
||||||
): Promise<void> {
|
|
||||||
const envPath = path.join(process.cwd(), ".env");
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let envContent = "";
|
let envContent = '';
|
||||||
try {
|
try {
|
||||||
envContent = await fs.readFile(envPath, "utf-8");
|
envContent = await fs.readFile(envPath, 'utf-8');
|
||||||
} catch {
|
} catch {
|
||||||
// .env file doesn't exist, we'll create it
|
// .env file doesn't exist, we'll create it
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse existing env content
|
// Parse existing env content
|
||||||
const lines = envContent.split("\n");
|
const lines = envContent.split('\n');
|
||||||
const keyRegex = new RegExp(`^${key}=`);
|
const keyRegex = new RegExp(`^${key}=`);
|
||||||
let found = false;
|
let found = false;
|
||||||
const newLines = lines.map((line) => {
|
const newLines = lines.map((line) => {
|
||||||
@@ -70,7 +64,7 @@ export async function persistApiKeyToEnv(
|
|||||||
newLines.push(`${key}=${value}`);
|
newLines.push(`${key}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
await fs.writeFile(envPath, newLines.join("\n"));
|
await fs.writeFile(envPath, newLines.join('\n'));
|
||||||
logger.info(`[Setup] Persisted ${key} to .env file`);
|
logger.info(`[Setup] Persisted ${key} to .env file`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`[Setup] Failed to persist ${key} to .env:`, error);
|
logger.error(`[Setup] Failed to persist ${key} to .env:`, error);
|
||||||
|
|||||||
@@ -2,36 +2,36 @@
|
|||||||
* Business logic for getting Claude CLI status
|
* Business logic for getting Claude CLI status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import os from "os";
|
import os from 'os';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import fs from 'fs/promises';
|
||||||
import { getApiKey } from "./common.js";
|
import { getApiKey } from './common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
export async function getClaudeStatus() {
|
export async function getClaudeStatus() {
|
||||||
let installed = false;
|
let installed = false;
|
||||||
let version = "";
|
let version = '';
|
||||||
let cliPath = "";
|
let cliPath = '';
|
||||||
let method = "none";
|
let method = 'none';
|
||||||
|
|
||||||
const isWindows = process.platform === "win32";
|
const isWindows = process.platform === 'win32';
|
||||||
|
|
||||||
// Try to find Claude CLI using platform-specific command
|
// Try to find Claude CLI using platform-specific command
|
||||||
try {
|
try {
|
||||||
// Use 'where' on Windows, 'which' on Unix-like systems
|
// Use 'where' on Windows, 'which' on Unix-like systems
|
||||||
const findCommand = isWindows ? "where claude" : "which claude";
|
const findCommand = isWindows ? 'where claude' : 'which claude';
|
||||||
const { stdout } = await execAsync(findCommand);
|
const { stdout } = await execAsync(findCommand);
|
||||||
// 'where' on Windows can return multiple paths - take the first one
|
// 'where' on Windows can return multiple paths - take the first one
|
||||||
cliPath = stdout.trim().split(/\r?\n/)[0];
|
cliPath = stdout.trim().split(/\r?\n/)[0];
|
||||||
installed = true;
|
installed = true;
|
||||||
method = "path";
|
method = 'path';
|
||||||
|
|
||||||
// Get version
|
// Get version
|
||||||
try {
|
try {
|
||||||
const { stdout: versionOut } = await execAsync("claude --version");
|
const { stdout: versionOut } = await execAsync('claude --version');
|
||||||
version = versionOut.trim();
|
version = versionOut.trim();
|
||||||
} catch {
|
} catch {
|
||||||
// Version command might not be available
|
// Version command might not be available
|
||||||
@@ -40,22 +40,22 @@ export async function getClaudeStatus() {
|
|||||||
// Not in PATH, try common locations based on platform
|
// Not in PATH, try common locations based on platform
|
||||||
const commonPaths = isWindows
|
const commonPaths = isWindows
|
||||||
? (() => {
|
? (() => {
|
||||||
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
||||||
return [
|
return [
|
||||||
// Windows-specific paths
|
// Windows-specific paths
|
||||||
path.join(os.homedir(), ".local", "bin", "claude.exe"),
|
path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
|
||||||
path.join(appData, "npm", "claude.cmd"),
|
path.join(appData, 'npm', 'claude.cmd'),
|
||||||
path.join(appData, "npm", "claude"),
|
path.join(appData, 'npm', 'claude'),
|
||||||
path.join(appData, ".npm-global", "bin", "claude.cmd"),
|
path.join(appData, '.npm-global', 'bin', 'claude.cmd'),
|
||||||
path.join(appData, ".npm-global", "bin", "claude"),
|
path.join(appData, '.npm-global', 'bin', 'claude'),
|
||||||
];
|
];
|
||||||
})()
|
})()
|
||||||
: [
|
: [
|
||||||
// Unix (Linux/macOS) paths
|
// Unix (Linux/macOS) paths
|
||||||
path.join(os.homedir(), ".local", "bin", "claude"),
|
path.join(os.homedir(), '.local', 'bin', 'claude'),
|
||||||
path.join(os.homedir(), ".claude", "local", "claude"),
|
path.join(os.homedir(), '.claude', 'local', 'claude'),
|
||||||
"/usr/local/bin/claude",
|
'/usr/local/bin/claude',
|
||||||
path.join(os.homedir(), ".npm-global", "bin", "claude"),
|
path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const p of commonPaths) {
|
for (const p of commonPaths) {
|
||||||
@@ -63,7 +63,7 @@ export async function getClaudeStatus() {
|
|||||||
await fs.access(p);
|
await fs.access(p);
|
||||||
cliPath = p;
|
cliPath = p;
|
||||||
installed = true;
|
installed = true;
|
||||||
method = "local";
|
method = 'local';
|
||||||
|
|
||||||
// Get version from this path
|
// Get version from this path
|
||||||
try {
|
try {
|
||||||
@@ -84,11 +84,11 @@ export async function getClaudeStatus() {
|
|||||||
// apiKeys.anthropic stores direct API keys for pay-per-use
|
// apiKeys.anthropic stores direct API keys for pay-per-use
|
||||||
let auth = {
|
let auth = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
method: "none" as string,
|
method: 'none' as string,
|
||||||
hasCredentialsFile: false,
|
hasCredentialsFile: false,
|
||||||
hasToken: false,
|
hasToken: false,
|
||||||
hasStoredOAuthToken: !!getApiKey("anthropic_oauth_token"),
|
hasStoredOAuthToken: !!getApiKey('anthropic_oauth_token'),
|
||||||
hasStoredApiKey: !!getApiKey("anthropic"),
|
hasStoredApiKey: !!getApiKey('anthropic'),
|
||||||
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
|
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
|
||||||
// Additional fields for detailed status
|
// Additional fields for detailed status
|
||||||
oauthTokenValid: false,
|
oauthTokenValid: false,
|
||||||
@@ -97,13 +97,13 @@ export async function getClaudeStatus() {
|
|||||||
hasRecentActivity: false,
|
hasRecentActivity: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const claudeDir = path.join(os.homedir(), ".claude");
|
const claudeDir = path.join(os.homedir(), '.claude');
|
||||||
|
|
||||||
// Check for recent Claude CLI activity - indicates working authentication
|
// Check for recent Claude CLI activity - indicates working authentication
|
||||||
// The stats-cache.json file is only populated when the CLI is working properly
|
// The stats-cache.json file is only populated when the CLI is working properly
|
||||||
const statsCachePath = path.join(claudeDir, "stats-cache.json");
|
const statsCachePath = path.join(claudeDir, 'stats-cache.json');
|
||||||
try {
|
try {
|
||||||
const statsContent = await fs.readFile(statsCachePath, "utf-8");
|
const statsContent = await fs.readFile(statsCachePath, 'utf-8');
|
||||||
const stats = JSON.parse(statsContent);
|
const stats = JSON.parse(statsContent);
|
||||||
|
|
||||||
// Check if there's any activity (which means the CLI is authenticated and working)
|
// Check if there's any activity (which means the CLI is authenticated and working)
|
||||||
@@ -111,26 +111,26 @@ export async function getClaudeStatus() {
|
|||||||
auth.hasRecentActivity = true;
|
auth.hasRecentActivity = true;
|
||||||
auth.hasCliAuth = true;
|
auth.hasCliAuth = true;
|
||||||
auth.authenticated = true;
|
auth.authenticated = true;
|
||||||
auth.method = "cli_authenticated";
|
auth.method = 'cli_authenticated';
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Stats file doesn't exist or is invalid
|
// Stats file doesn't exist or is invalid
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for settings.json - indicates CLI has been set up
|
// Check for settings.json - indicates CLI has been set up
|
||||||
const settingsPath = path.join(claudeDir, "settings.json");
|
const settingsPath = path.join(claudeDir, 'settings.json');
|
||||||
try {
|
try {
|
||||||
await fs.access(settingsPath);
|
await fs.access(settingsPath);
|
||||||
// If settings exist but no activity, CLI might be set up but not authenticated
|
// If settings exist but no activity, CLI might be set up but not authenticated
|
||||||
if (!auth.hasCliAuth) {
|
if (!auth.hasCliAuth) {
|
||||||
// Try to check for other indicators of auth
|
// Try to check for other indicators of auth
|
||||||
const sessionsDir = path.join(claudeDir, "projects");
|
const sessionsDir = path.join(claudeDir, 'projects');
|
||||||
try {
|
try {
|
||||||
const sessions = await fs.readdir(sessionsDir);
|
const sessions = await fs.readdir(sessionsDir);
|
||||||
if (sessions.length > 0) {
|
if (sessions.length > 0) {
|
||||||
auth.hasCliAuth = true;
|
auth.hasCliAuth = true;
|
||||||
auth.authenticated = true;
|
auth.authenticated = true;
|
||||||
auth.method = "cli_authenticated";
|
auth.method = 'cli_authenticated';
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Sessions directory doesn't exist
|
// Sessions directory doesn't exist
|
||||||
@@ -143,13 +143,13 @@ export async function getClaudeStatus() {
|
|||||||
// Check for credentials file (OAuth tokens from claude login)
|
// Check for credentials file (OAuth tokens from claude login)
|
||||||
// Note: Claude CLI may use ".credentials.json" (hidden) or "credentials.json" depending on version/platform
|
// Note: Claude CLI may use ".credentials.json" (hidden) or "credentials.json" depending on version/platform
|
||||||
const credentialsPaths = [
|
const credentialsPaths = [
|
||||||
path.join(claudeDir, ".credentials.json"),
|
path.join(claudeDir, '.credentials.json'),
|
||||||
path.join(claudeDir, "credentials.json"),
|
path.join(claudeDir, 'credentials.json'),
|
||||||
];
|
];
|
||||||
|
|
||||||
for (const credentialsPath of credentialsPaths) {
|
for (const credentialsPath of credentialsPaths) {
|
||||||
try {
|
try {
|
||||||
const credentialsContent = await fs.readFile(credentialsPath, "utf-8");
|
const credentialsContent = await fs.readFile(credentialsPath, 'utf-8');
|
||||||
const credentials = JSON.parse(credentialsContent);
|
const credentials = JSON.parse(credentialsContent);
|
||||||
auth.hasCredentialsFile = true;
|
auth.hasCredentialsFile = true;
|
||||||
|
|
||||||
@@ -158,11 +158,11 @@ export async function getClaudeStatus() {
|
|||||||
auth.hasStoredOAuthToken = true;
|
auth.hasStoredOAuthToken = true;
|
||||||
auth.oauthTokenValid = true;
|
auth.oauthTokenValid = true;
|
||||||
auth.authenticated = true;
|
auth.authenticated = true;
|
||||||
auth.method = "oauth_token"; // Stored OAuth token from credentials file
|
auth.method = 'oauth_token'; // Stored OAuth token from credentials file
|
||||||
} else if (credentials.api_key) {
|
} else if (credentials.api_key) {
|
||||||
auth.apiKeyValid = true;
|
auth.apiKeyValid = true;
|
||||||
auth.authenticated = true;
|
auth.authenticated = true;
|
||||||
auth.method = "api_key"; // Stored API key in credentials file
|
auth.method = 'api_key'; // Stored API key in credentials file
|
||||||
}
|
}
|
||||||
break; // Found and processed credentials file
|
break; // Found and processed credentials file
|
||||||
} catch {
|
} catch {
|
||||||
@@ -174,25 +174,25 @@ export async function getClaudeStatus() {
|
|||||||
if (auth.hasEnvApiKey) {
|
if (auth.hasEnvApiKey) {
|
||||||
auth.authenticated = true;
|
auth.authenticated = true;
|
||||||
auth.apiKeyValid = true;
|
auth.apiKeyValid = true;
|
||||||
auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var
|
auth.method = 'api_key_env'; // API key from ANTHROPIC_API_KEY env var
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-memory stored OAuth token (from setup wizard - subscription auth)
|
// In-memory stored OAuth token (from setup wizard - subscription auth)
|
||||||
if (!auth.authenticated && getApiKey("anthropic_oauth_token")) {
|
if (!auth.authenticated && getApiKey('anthropic_oauth_token')) {
|
||||||
auth.authenticated = true;
|
auth.authenticated = true;
|
||||||
auth.oauthTokenValid = true;
|
auth.oauthTokenValid = true;
|
||||||
auth.method = "oauth_token"; // Stored OAuth token from setup wizard
|
auth.method = 'oauth_token'; // Stored OAuth token from setup wizard
|
||||||
}
|
}
|
||||||
|
|
||||||
// In-memory stored API key (from settings UI - pay-per-use)
|
// In-memory stored API key (from settings UI - pay-per-use)
|
||||||
if (!auth.authenticated && getApiKey("anthropic")) {
|
if (!auth.authenticated && getApiKey('anthropic')) {
|
||||||
auth.authenticated = true;
|
auth.authenticated = true;
|
||||||
auth.apiKeyValid = true;
|
auth.apiKeyValid = true;
|
||||||
auth.method = "api_key"; // Manually stored API key
|
auth.method = 'api_key'; // Manually stored API key
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: installed ? "installed" : "not_installed",
|
status: installed ? 'installed' : 'not_installed',
|
||||||
installed,
|
installed,
|
||||||
method,
|
method,
|
||||||
version,
|
version,
|
||||||
|
|||||||
@@ -2,29 +2,29 @@
|
|||||||
* Setup routes - HTTP API for CLI detection, API keys, and platform info
|
* Setup routes - HTTP API for CLI detection, API keys, and platform info
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Router } from "express";
|
import { Router } from 'express';
|
||||||
import { createClaudeStatusHandler } from "./routes/claude-status.js";
|
import { createClaudeStatusHandler } from './routes/claude-status.js';
|
||||||
import { createInstallClaudeHandler } from "./routes/install-claude.js";
|
import { createInstallClaudeHandler } from './routes/install-claude.js';
|
||||||
import { createAuthClaudeHandler } from "./routes/auth-claude.js";
|
import { createAuthClaudeHandler } from './routes/auth-claude.js';
|
||||||
import { createStoreApiKeyHandler } from "./routes/store-api-key.js";
|
import { createStoreApiKeyHandler } from './routes/store-api-key.js';
|
||||||
import { createDeleteApiKeyHandler } from "./routes/delete-api-key.js";
|
import { createDeleteApiKeyHandler } from './routes/delete-api-key.js';
|
||||||
import { createApiKeysHandler } from "./routes/api-keys.js";
|
import { createApiKeysHandler } from './routes/api-keys.js';
|
||||||
import { createPlatformHandler } from "./routes/platform.js";
|
import { createPlatformHandler } from './routes/platform.js';
|
||||||
import { createVerifyClaudeAuthHandler } from "./routes/verify-claude-auth.js";
|
import { createVerifyClaudeAuthHandler } from './routes/verify-claude-auth.js';
|
||||||
import { createGhStatusHandler } from "./routes/gh-status.js";
|
import { createGhStatusHandler } from './routes/gh-status.js';
|
||||||
|
|
||||||
export function createSetupRoutes(): Router {
|
export function createSetupRoutes(): Router {
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get("/claude-status", createClaudeStatusHandler());
|
router.get('/claude-status', createClaudeStatusHandler());
|
||||||
router.post("/install-claude", createInstallClaudeHandler());
|
router.post('/install-claude', createInstallClaudeHandler());
|
||||||
router.post("/auth-claude", createAuthClaudeHandler());
|
router.post('/auth-claude', createAuthClaudeHandler());
|
||||||
router.post("/store-api-key", createStoreApiKeyHandler());
|
router.post('/store-api-key', createStoreApiKeyHandler());
|
||||||
router.post("/delete-api-key", createDeleteApiKeyHandler());
|
router.post('/delete-api-key', createDeleteApiKeyHandler());
|
||||||
router.get("/api-keys", createApiKeysHandler());
|
router.get('/api-keys', createApiKeysHandler());
|
||||||
router.get("/platform", createPlatformHandler());
|
router.get('/platform', createPlatformHandler());
|
||||||
router.post("/verify-claude-auth", createVerifyClaudeAuthHandler());
|
router.post('/verify-claude-auth', createVerifyClaudeAuthHandler());
|
||||||
router.get("/gh-status", createGhStatusHandler());
|
router.get('/gh-status', createGhStatusHandler());
|
||||||
|
|
||||||
return router;
|
return router;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,19 +2,18 @@
|
|||||||
* GET /api-keys endpoint - Get API keys status
|
* GET /api-keys endpoint - Get API keys status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { getApiKey, getErrorMessage, logError } from "../common.js";
|
import { getApiKey, getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createApiKeysHandler() {
|
export function createApiKeysHandler() {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
hasAnthropicKey:
|
hasAnthropicKey: !!getApiKey('anthropic') || !!process.env.ANTHROPIC_API_KEY,
|
||||||
!!getApiKey("anthropic") || !!process.env.ANTHROPIC_API_KEY,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get API keys failed");
|
logError(error, 'Get API keys failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user