Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,13 @@ AUTO_APPROVE_FILE_EDITS=false
# (Optional) Override path to the Antigravity CLI binary
# ANTIGRAVITY_PATH=/usr/local/bin/antigravity

# (Optional) Custom CDP debugging port override (default: auto-scanned ports 9222, 9223, 9333, etc.)
# CDP_PORT=9888

# (Optional) Multi-instance Antigravity account mappings (format: name:port[@userDataDir],...)
# ANTIGRAVITY_ACCOUNTS=default:9222,work:9333@/path/to/user-data-dir


# Response delivery path is stream-only (final-only mode removed)

# --- Multi-Platform Support ---
Expand Down
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ Just type in any bound channel:
- `📝 /template list` — Display registered templates with execute buttons
- `📝 /template add <name> <prompt>` — Register a new prompt template
- `📝 /template delete <name>` — Delete a template
- `📝 /template export` — Export all prompt templates as a JSON file attachment
- `📝 /template import <file> [conflict]` — Bulk-import templates from a JSON file attachment

- `📅 /schedule list` — Show all scheduled tasks with next localized run times
- `📅 /schedule add <cron> <prompt>` — Register a recurring task for the current channel's bound project
- `📅 /schedule remove <id>` — Delete a scheduled task by ID
Expand Down Expand Up @@ -246,9 +249,12 @@ On Linux (especially when using AppImages), the `antigravity` command might not
You can specify the exact path to your executable by setting the `ANTIGRAVITY_PATH` environment variable in your `.env` file:
```env
ANTIGRAVITY_PATH=/opt/applications/antigravity.AppImage
# Optional: Specify a custom CDP port or multi-instance account mappings
CDP_PORT=9888
ANTIGRAVITY_ACCOUNTS=default:9222,work:9333@/path/to/user-data-dir
```

> **Tip**: CDP ports are auto-scanned from candidates (9222, 9223, 9333, 9444, 9555, 9666).
> **Tip**: CDP ports are auto-scanned from candidates (9222, 9223, 9333, 9444, 9555, 9666), as well as any custom ports specified in `.env` via `CDP_PORT` or `ANTIGRAVITY_ACCOUNTS`.
> Launch Antigravity first, then start the bot — it connects automatically.

---
Expand Down
17 changes: 7 additions & 10 deletions docs/ANTIGRAVITY_DOM_SELECTORS.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ The assistant's response body, rendered with markdown formatting.
| 10 | `.rendered-markdown` | **Verified** | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 9 | `.leading-relaxed.select-text` | **Verified** | `responseMonitor.ts`, `planningDetector.ts`, `assistantDomExtractor.ts` |
| 8 | `.flex.flex-col.gap-y-3` | **Deprecated** — removed from primary extraction paths (AG 1.23.x) | — |
| 7 | `[data-message-author-role="assistant"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 6 | `[data-message-role="assistant"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 5 | `[class*="assistant-message"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 4 | `[class*="message-content"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| 3 | `[class*="markdown-body"]` | **NOT FOUND** in DOM | `responseMonitor.ts`, `assistantDomExtractor.ts` |
| | `[data-message-author-role="assistant"]` | **Removed** (Issue #40 cleanup) | — |
| | `[data-message-role="assistant"]` | **Removed** (Issue #40 cleanup) | — |
| | `[class*="assistant-message"]` | **Removed** (Issue #40 cleanup) | — |
| | `[class*="message-content"]` | **Removed** (Issue #40 cleanup) | — |
| | `[class*="markdown-body"]` | **Removed** (Issue #40 cleanup) | — |
| 2 | `.prose` | Unverified | `responseMonitor.ts`, `assistantDomExtractor.ts` |

> **Note**: Selectors scored 3-7 appear to be inherited from ChatGPT/generic patterns and do **not** exist in Antigravity's DOM. They are harmless (scored lower, never matched) but add noise. The top selectors (`.text-ide-message-block-bot-color`, `.rendered-markdown`, `.leading-relaxed.select-text`) are the ones that actually match.
> **Note**: Selectors scored 3-7 inherited from ChatGPT/generic patterns were removed in Issue #40 cleanup. The active selectors (`.text-ide-message-block-bot-color`, `.rendered-markdown`, `.leading-relaxed.select-text`, `.prose`) handle response extraction.

### Exclusion Containers

Expand Down Expand Up @@ -246,10 +246,7 @@ Model quota reached / rate limit detection.
Quota text is only matched outside response containers to avoid false positives:

```
.rendered-markdown, .prose, pre, code,
[data-message-author-role="assistant"],
[data-message-role="assistant"],
[class*="message-content"]
.rendered-markdown, .prose, pre, code
```

---
Expand Down
3 changes: 2 additions & 1 deletion locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,6 @@
"Fast Mode — for simple tasks": "Fast Mode — for simple tasks",
"Plan Mode — for complex step-by-step tasks": "Plan Mode — for complex step-by-step tasks",
"⚠️ Mode name not specified. Available modes: ": "⚠️ Mode name not specified. Available modes: ",
"⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}": "⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}"
"⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}": "⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}",
"📂 Artifacts": "📂 Artifacts"
}
3 changes: 2 additions & 1 deletion locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,5 +105,6 @@
"Fast Mode — for simple tasks": "高速応答モード — シンプルなタスク向け",
"Plan Mode — for complex step-by-step tasks": "計画モード — 複雑なタスクを段階的に実行",
"⚠️ Mode name not specified. Available modes: ": "⚠️ モード名が指定されていません。利用可能なモード: ",
"⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}": "⚠️ 無効なモード \"${modeName}\" です。利用可能なモード: ${AVAILABLE_MODES.join(', ')}"
"⚠️ Invalid mode \"${modeName}\". Available modes: ${AVAILABLE_MODES.join(', ')}": "⚠️ 無効なモード \"${modeName}\" です。利用可能なモード: ${AVAILABLE_MODES.join(', ')}",
"📂 Artifacts": "📂 成果物一覧"
}
6 changes: 3 additions & 3 deletions src/bin/commands/doctor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import * as http from 'http';
import * as fs from 'fs';
import * as path from 'path';
import { CDP_PORTS } from '../../utils/cdpPorts';
import { getCdpCandidatePorts } from '../../utils/cdpPorts';
import { ConfigLoader } from '../../utils/configLoader';
import { getAntigravityCdpHint } from '../../utils/pathUtils';
import { COLORS } from '../../utils/logger';
Expand Down Expand Up @@ -154,7 +154,7 @@ export async function doctorAction(): Promise<void> {
console.log(`\n ${COLORS.dim}Checking CDP ports...${COLORS.reset}`);
let cdpOk = false;
const portResults = new Map<number, any[]>();
for (const port of CDP_PORTS) {
for (const port of getCdpCandidatePorts()) {
const result = await checkPort(port);
if (result.alive) {
ok(`CDP port ${port} is responding`);
Expand All @@ -176,7 +176,7 @@ export async function doctorAction(): Promise<void> {
const resolvedPath = artifactService.getBrainBasePath();
ok(`Resolved brainBasePath: ${resolvedPath}`);

for (const port of CDP_PORTS) {
for (const port of getCdpCandidatePorts()) {
const targets = portResults.get(port);
if (targets && Array.isArray(targets)) {
for (const t of targets) {
Expand Down
6 changes: 3 additions & 3 deletions src/bin/commands/open.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as net from 'net';
import * as http from 'http';
import * as os from 'os';
import { execFile, spawn } from 'child_process';
import { CDP_PORTS } from '../../utils/cdpPorts';
import { getCdpCandidatePorts } from '../../utils/cdpPorts';

const APP_NAME = 'Antigravity';

Expand Down Expand Up @@ -37,7 +37,7 @@ function isPortAvailable(port: number): Promise<boolean> {
* @returns Available port number, or null if none are available.
*/
async function findAvailablePort(): Promise<number | null> {
for (const port of CDP_PORTS) {
for (const port of getCdpCandidatePorts()) {
if (await isPortAvailable(port)) {
return port;
}
Expand Down Expand Up @@ -159,7 +159,7 @@ export async function openAction(): Promise<void> {
const port = await findAvailablePort();
if (port === null) {
console.log(` ${C.red}No available CDP ports found.${C.reset}`);
console.log(` ${C.dim}All candidate ports are in use: ${CDP_PORTS.join(', ')}${C.reset}`);
console.log(` ${C.dim}All candidate ports are in use: ${getCdpCandidatePorts().join(', ')}${C.reset}`);
console.log(` ${C.dim}Close an application using one of these ports and try again.${C.reset}\n`);
process.exitCode = 1;
return;
Expand Down
4 changes: 2 additions & 2 deletions src/bin/commands/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ async function getSelect(): Promise<SelectFn> {
}
import { ConfigLoader } from '../../utils/configLoader';
import type { PersistedConfig } from '../../utils/configLoader';
import { CDP_PORTS } from '../../utils/cdpPorts';
import { getCdpCandidatePorts } from '../../utils/cdpPorts';
import type { PlatformType } from '../../platform/types';

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -771,7 +771,7 @@ export async function setupAction(): Promise<void> {
console.log(` ${C.cyan}Start:${C.reset}`);
console.log(` ${C.bold}1.${C.reset} Open Antigravity with CDP enabled:`);
console.log(` ${C.green}lazy-gravity open${C.reset}`);
console.log(` ${C.dim}(auto-selects an available port from: ${CDP_PORTS.join(', ')})${C.reset}\n`);
console.log(` ${C.dim}(auto-selects an available port from: ${getCdpCandidatePorts().join(', ')})${C.reset}\n`);
console.log(` ${C.bold}2.${C.reset} Run: ${C.green}lazy-gravity start${C.reset}\n`);

return;
Expand Down
111 changes: 105 additions & 6 deletions src/bot/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ import {
import { buildModeModelLines, fitForSingleEmbedDescription, splitForEmbedDescription } from '../utils/streamMessageFormatter';
import { formatForDiscord, splitOutputAndLogs } from '../utils/discordFormatter';
import { renderDiscordResponse } from '../platform/discord/discordResponseRenderer';
import { ProcessLogBuffer } from '../utils/processLogBuffer';
import { ProcessLogBuffer, createDefaultProcessLogBuffer } from '../utils/processLogBuffer';
import {
buildPromptWithAttachmentUrls,
cleanupInboundImageAttachments,
Expand Down Expand Up @@ -453,10 +453,8 @@ async function sendPromptToAntigravity(
let lastActivityLogText = '';
const LIVE_RESPONSE_MAX_LEN = 3800;
const LIVE_ACTIVITY_MAX_LEN = 3800;
const processLogBuffer = new ProcessLogBuffer({
const processLogBuffer = createDefaultProcessLogBuffer({
maxChars: LIVE_ACTIVITY_MAX_LEN,
maxEntries: 120,
maxEntryLength: 220,
});
const liveResponseMessages: any[] = [];
const liveActivityMessages: any[] = [];
Expand Down Expand Up @@ -2318,8 +2316,11 @@ export async function handleSlashInteraction(
'`/template list` — Show templates with execute buttons (click to run)',
'`/template add <name> <prompt>` — Register a template',
'`/template delete <name>` — Delete a template',
'`/template export` — Export all templates as a JSON file',
'`/template import` — Bulk-import templates from a JSON file',
].join('\n')
},

{
name: '🔧 System', value: [
'`/status` — Display overall bot status',
Expand Down Expand Up @@ -2426,6 +2427,67 @@ export async function handleSlashInteraction(
break;
}

if (subcommand === 'export') {
const templates = templateRepo.findAll();
if (templates.length === 0) {
await interaction.editReply({ content: '📝 No templates registered to export.' });
break;
}
const jsonStr = templateRepo.exportTemplates();
const buffer = Buffer.from(jsonStr, 'utf-8');
await interaction.editReply({
content: '📋 **LazyGravity Templates Export**',
files: [{
attachment: buffer,
name: 'templates_export.json'
}]
});
break;
}

if (subcommand === 'import') {
const attachment = interaction.options.getAttachment('file', true);
const conflictMode = (interaction.options.getString('conflict') as 'skip' | 'overwrite') || 'skip';

if (!attachment.name.endsWith('.json')) {
await interaction.editReply({ content: '❌ Attachment must be a `.json` file.' });
break;
}

if (attachment.size > 1024 * 1024) {
await interaction.editReply({ content: '❌ Attachment exceeds maximum size limit of 1MB.' });
break;
}

let timeoutId: NodeJS.Timeout | undefined;
try {
const controller = new AbortController();
timeoutId = setTimeout(() => controller.abort(), 10000);

const response = await fetch(attachment.url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);

const jsonText = await response.text();
const stats = templateRepo.importTemplates(jsonText, conflictMode);

let msg = `✅ **Template Import Complete**\n`;
msg += `- Total in file: ${stats.total}\n`;
msg += `- Imported: ${stats.imported}\n`;
if (conflictMode === 'overwrite') {
msg += `- Overwritten: ${stats.updated}\n`;
} else {
msg += `- Skipped (duplicates): ${stats.skipped}\n`;
}

await interaction.editReply({ content: msg });
} catch (error: any) {
await interaction.editReply({ content: `❌ Failed to import templates: ${error.message}` });
} finally {
if (timeoutId) clearTimeout(timeoutId);
}
break;
}

let args: string[];
switch (subcommand) {
case 'add': {
Expand All @@ -2448,6 +2510,7 @@ export async function handleSlashInteraction(
break;
}


case 'status': {
const activeNames = bridge.pool.getActiveWorkspaceNames();
const currentModel = (() => {
Expand Down Expand Up @@ -3094,12 +3157,48 @@ export async function handleSlashInteraction(
const response = await fetch(attachment.url, { signal: controller.signal });
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);

const MAX_BYTES = 1024 * 1024; // 1 MiB
const contentLength = response.headers.get('content-length');
if (contentLength && parseInt(contentLength, 10) > 1024 * 1024) {
if (contentLength && parseInt(contentLength, 10) > MAX_BYTES) {
throw new Error('Response body exceeds maximum size limit of 1MB.');
}

jsonText = await response.text();
if (response.body && typeof (response.body as any).getReader === 'function') {
const reader = (response.body as any).getReader();
const chunks: Uint8Array[] = [];
let totalBytes = 0;

try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
if (value) {
totalBytes += value.length;
if (totalBytes > MAX_BYTES) {
controller.abort();
throw new Error('Response body exceeds maximum size limit of 1MB.');
}
chunks.push(value);
}
}
} finally {
if (reader.releaseLock) reader.releaseLock();
}

const combined = new Uint8Array(totalBytes);
let offset = 0;
for (const chunk of chunks) {
combined.set(chunk, offset);
offset += chunk.length;
}
jsonText = new TextDecoder().decode(combined);
} else {
const arrayBuffer = await response.arrayBuffer();
if (arrayBuffer.byteLength > MAX_BYTES) {
throw new Error('Response body exceeds maximum size limit of 1MB.');
}
jsonText = new TextDecoder().decode(arrayBuffer);
}
} finally {
if (timeoutId) {
clearTimeout(timeoutId);
Expand Down
4 changes: 2 additions & 2 deletions src/bot/telegramMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import type { WorkspaceService } from '../services/workspaceService';
import { CdpBridge, registerApprovalWorkspaceChannel, ensureApprovalDetector, ensureErrorPopupDetector, ensurePlanningDetector, ensureRunCommandDetector, ensureQuestionDetector } from '../services/cdpBridgeManager';
import { CdpService } from '../services/cdpService';
import { ResponseMonitor, captureResponseMonitorBaseline } from '../services/responseMonitor';
import { ProcessLogBuffer } from '../utils/processLogBuffer';
import { ProcessLogBuffer, createDefaultProcessLogBuffer } from '../utils/processLogBuffer';
import { splitOutputAndLogs } from '../utils/discordFormatter';
import { parseTelegramProjectCommand, handleTelegramProjectCommand } from './telegramProjectCommand';
import { parseTelegramCommand, handleTelegramCommand } from './telegramCommands';
Expand Down Expand Up @@ -297,7 +297,7 @@ export function createTelegramMessageHandler(deps: TelegramMessageHandlerDeps) {
// Monitor the response
const channel = message.channel;
const startTime = Date.now();
const processLogBuffer = new ProcessLogBuffer({ maxChars: 3500, maxEntries: 120, maxEntryLength: 220 });
const processLogBuffer = createDefaultProcessLogBuffer();
let lastActivityLogText = '';
let statusMsg: PlatformSentMessage | null = null;

Expand Down
Loading