paseo-acp-agy 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/PROTOCOL.md +149 -0
- package/README.md +137 -0
- package/dist/acp-server.d.ts +28 -0
- package/dist/acp-server.js +575 -0
- package/dist/antigravity-process.d.ts +52 -0
- package/dist/antigravity-process.js +535 -0
- package/dist/attachments.d.ts +1 -0
- package/dist/attachments.js +102 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +51 -0
- package/dist/logger.d.ts +19 -0
- package/dist/logger.js +124 -0
- package/dist/permissions.d.ts +11 -0
- package/dist/permissions.js +29 -0
- package/dist/protocol.d.ts +201 -0
- package/dist/protocol.js +471 -0
- package/dist/session-store.d.ts +27 -0
- package/dist/session-store.js +74 -0
- package/dist/session.d.ts +66 -0
- package/dist/session.js +227 -0
- package/dist/slash-commands.d.ts +33 -0
- package/dist/slash-commands.js +434 -0
- package/dist/version.d.ts +17 -0
- package/dist/version.js +95 -0
- package/package.json +55 -0
package/dist/session.js
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import { AntigravityProcess } from "./antigravity-process.js";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
import { resolvePermissionSettings } from "./permissions.js";
|
|
5
|
+
import { calculateUsageCostUsd, getModelContextWindow } from "./protocol.js";
|
|
6
|
+
import { sessionStore, } from "./session-store.js";
|
|
7
|
+
export class Session {
|
|
8
|
+
id;
|
|
9
|
+
cwd;
|
|
10
|
+
model;
|
|
11
|
+
effort;
|
|
12
|
+
mode;
|
|
13
|
+
createdAt;
|
|
14
|
+
lastActivity;
|
|
15
|
+
isCancelled = false;
|
|
16
|
+
process;
|
|
17
|
+
usage;
|
|
18
|
+
store;
|
|
19
|
+
promptOperationActive = false;
|
|
20
|
+
constructor(options = {}) {
|
|
21
|
+
this.id = options.id || crypto.randomUUID();
|
|
22
|
+
this.cwd = options.cwd || process.cwd();
|
|
23
|
+
this.model = options.model || "gemini-3.7-flash";
|
|
24
|
+
this.effort = options.effort || "high";
|
|
25
|
+
this.mode = options.mode || "default";
|
|
26
|
+
this.createdAt = new Date();
|
|
27
|
+
this.lastActivity = new Date();
|
|
28
|
+
this.store = options.store || sessionStore;
|
|
29
|
+
this.usage = options.usage || {
|
|
30
|
+
inputTokens: 0,
|
|
31
|
+
outputTokens: 0,
|
|
32
|
+
cachedInputTokens: 0,
|
|
33
|
+
totalTokens: 0,
|
|
34
|
+
totalCostUsd: 0,
|
|
35
|
+
contextWindowUsedTokens: 0,
|
|
36
|
+
contextWindowMaxTokens: getModelContextWindow(this.model),
|
|
37
|
+
};
|
|
38
|
+
const permissions = resolvePermissionSettings({
|
|
39
|
+
sandbox: options.sandbox,
|
|
40
|
+
dangerouslySkipPermissions: options.dangerouslySkipPermissions,
|
|
41
|
+
mode: this.mode,
|
|
42
|
+
});
|
|
43
|
+
this.process = new AntigravityProcess({
|
|
44
|
+
binaryPath: options.binaryPath,
|
|
45
|
+
cwd: this.cwd,
|
|
46
|
+
model: this.model,
|
|
47
|
+
effort: this.effort,
|
|
48
|
+
mode: this.mode,
|
|
49
|
+
conversationId: options.conversationId,
|
|
50
|
+
permissions,
|
|
51
|
+
env: options.env,
|
|
52
|
+
});
|
|
53
|
+
this.process.on("init", () => this.persist());
|
|
54
|
+
this.process.on("result", () => this.persist());
|
|
55
|
+
this.persist();
|
|
56
|
+
}
|
|
57
|
+
persistedState() {
|
|
58
|
+
return {
|
|
59
|
+
sessionId: this.id,
|
|
60
|
+
conversationId: this.process.currentConversationId,
|
|
61
|
+
cwd: this.cwd,
|
|
62
|
+
model: this.model,
|
|
63
|
+
effort: this.effort,
|
|
64
|
+
mode: this.mode,
|
|
65
|
+
usage: this.usage,
|
|
66
|
+
updatedAt: new Date().toISOString(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
persist() {
|
|
70
|
+
this.store.save(this.persistedState());
|
|
71
|
+
}
|
|
72
|
+
get isPromptOperationActive() {
|
|
73
|
+
return this.promptOperationActive;
|
|
74
|
+
}
|
|
75
|
+
tryBeginPromptOperation() {
|
|
76
|
+
if (this.promptOperationActive)
|
|
77
|
+
return false;
|
|
78
|
+
this.promptOperationActive = true;
|
|
79
|
+
this.isCancelled = false;
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
endPromptOperation() {
|
|
83
|
+
this.promptOperationActive = false;
|
|
84
|
+
}
|
|
85
|
+
setModel(model) {
|
|
86
|
+
this.model = model;
|
|
87
|
+
this.usage.contextWindowMaxTokens = getModelContextWindow(model);
|
|
88
|
+
this.process.setModel(model);
|
|
89
|
+
this.persist();
|
|
90
|
+
}
|
|
91
|
+
setEffort(effort) {
|
|
92
|
+
this.effort = effort;
|
|
93
|
+
this.process.setEffort(effort);
|
|
94
|
+
this.persist();
|
|
95
|
+
}
|
|
96
|
+
setMode(mode) {
|
|
97
|
+
this.mode = mode;
|
|
98
|
+
this.process.setMode(mode);
|
|
99
|
+
this.persist();
|
|
100
|
+
}
|
|
101
|
+
setConversationId(conversationId) {
|
|
102
|
+
this.process.setConversationId(conversationId);
|
|
103
|
+
this.persist();
|
|
104
|
+
}
|
|
105
|
+
async ensureReadyForResume() {
|
|
106
|
+
const conversationId = this.process.currentConversationId;
|
|
107
|
+
if (!conversationId) {
|
|
108
|
+
throw new Error(`ACP session ${this.id} has no persisted Antigravity conversation ID`);
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
await this.process.ensureReady(conversationId);
|
|
112
|
+
this.persist();
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
this.persist();
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
async resumeConversation(conversationId) {
|
|
120
|
+
const previousConversationId = this.process.currentConversationId;
|
|
121
|
+
this.setConversationId(conversationId);
|
|
122
|
+
try {
|
|
123
|
+
await this.ensureReadyForResume();
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
// A failed manual /resume must not poison the current Paseo session.
|
|
127
|
+
this.setConversationId(previousConversationId);
|
|
128
|
+
this.persist();
|
|
129
|
+
throw err;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
recordTurnUsage(turnUsage, executingModel = this.model) {
|
|
133
|
+
if (!turnUsage)
|
|
134
|
+
return;
|
|
135
|
+
const input = turnUsage.input_tokens || 0;
|
|
136
|
+
const output = turnUsage.output_tokens || 0;
|
|
137
|
+
const cached = turnUsage.cache_read_tokens || 0;
|
|
138
|
+
const turnCost = calculateUsageCostUsd(executingModel, input, output, cached);
|
|
139
|
+
this.usage.inputTokens += input;
|
|
140
|
+
this.usage.outputTokens += output;
|
|
141
|
+
this.usage.cachedInputTokens += cached;
|
|
142
|
+
this.usage.totalTokens = this.usage.inputTokens + this.usage.outputTokens;
|
|
143
|
+
// Keep the accumulator unrounded. Micro-costs must survive across turns.
|
|
144
|
+
this.usage.totalCostUsd += turnCost;
|
|
145
|
+
this.usage.contextWindowUsedTokens = input + output;
|
|
146
|
+
// The selected model may have changed while this turn was running. Keep
|
|
147
|
+
// the stored meter aligned with the model currently selected for next turn.
|
|
148
|
+
this.usage.contextWindowMaxTokens = getModelContextWindow(this.model);
|
|
149
|
+
this.persist();
|
|
150
|
+
}
|
|
151
|
+
touch() {
|
|
152
|
+
this.lastActivity = new Date();
|
|
153
|
+
}
|
|
154
|
+
cancelTurn() {
|
|
155
|
+
const processCancelled = this.process.cancelCurrentTurn();
|
|
156
|
+
const cancelled = this.promptOperationActive || processCancelled;
|
|
157
|
+
if (cancelled)
|
|
158
|
+
this.isCancelled = true;
|
|
159
|
+
return cancelled;
|
|
160
|
+
}
|
|
161
|
+
async close() {
|
|
162
|
+
logger.info("Closing session", { sessionId: this.id });
|
|
163
|
+
this.persist();
|
|
164
|
+
this.promptOperationActive = false;
|
|
165
|
+
await this.process.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export class SessionManager {
|
|
169
|
+
sessions = new Map();
|
|
170
|
+
defaultBinaryPath;
|
|
171
|
+
store;
|
|
172
|
+
constructor(options) {
|
|
173
|
+
this.defaultBinaryPath = options?.defaultBinaryPath;
|
|
174
|
+
this.store = options?.store || sessionStore;
|
|
175
|
+
}
|
|
176
|
+
createSession(options = {}) {
|
|
177
|
+
const persisted = options.id ? this.store.load(options.id) : null;
|
|
178
|
+
if (options.id && !persisted) {
|
|
179
|
+
throw new Error(`Cannot restore unknown ACP session: ${options.id}`);
|
|
180
|
+
}
|
|
181
|
+
const session = new Session({
|
|
182
|
+
id: options.id,
|
|
183
|
+
binaryPath: options.binaryPath || this.defaultBinaryPath,
|
|
184
|
+
cwd: options.cwd || persisted?.cwd,
|
|
185
|
+
model: options.model || persisted?.model,
|
|
186
|
+
effort: options.effort || persisted?.effort,
|
|
187
|
+
mode: options.mode || persisted?.mode,
|
|
188
|
+
conversationId: options.conversationId || persisted?.conversationId,
|
|
189
|
+
usage: options.usage || persisted?.usage,
|
|
190
|
+
sandbox: options.sandbox,
|
|
191
|
+
dangerouslySkipPermissions: options.dangerouslySkipPermissions,
|
|
192
|
+
env: options.env,
|
|
193
|
+
store: this.store,
|
|
194
|
+
});
|
|
195
|
+
this.sessions.set(session.id, session);
|
|
196
|
+
logger.info("Created new session", {
|
|
197
|
+
sessionId: session.id,
|
|
198
|
+
cwd: session.cwd,
|
|
199
|
+
model: session.model,
|
|
200
|
+
effort: session.effort,
|
|
201
|
+
resumedConversation: Boolean(session.process.currentConversationId),
|
|
202
|
+
});
|
|
203
|
+
return session;
|
|
204
|
+
}
|
|
205
|
+
getSession(id) {
|
|
206
|
+
return this.sessions.get(id);
|
|
207
|
+
}
|
|
208
|
+
hasPersistedSession(id) {
|
|
209
|
+
return this.store.load(id) !== null;
|
|
210
|
+
}
|
|
211
|
+
async closeSession(id) {
|
|
212
|
+
const session = this.sessions.get(id);
|
|
213
|
+
if (!session)
|
|
214
|
+
return false;
|
|
215
|
+
await session.close();
|
|
216
|
+
this.sessions.delete(id);
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
async closeAll() {
|
|
220
|
+
logger.info("Closing all active sessions", { count: this.sessions.size });
|
|
221
|
+
await Promise.allSettled(Array.from(this.sessions.values(), (session) => session.close()));
|
|
222
|
+
this.sessions.clear();
|
|
223
|
+
}
|
|
224
|
+
listSessions() {
|
|
225
|
+
return Array.from(this.sessions.values());
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Session } from "./session.js";
|
|
2
|
+
export interface SlashCommandDefinition {
|
|
3
|
+
name: string;
|
|
4
|
+
description: string;
|
|
5
|
+
argumentHint?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare const AVAILABLE_SLASH_COMMANDS: SlashCommandDefinition[];
|
|
8
|
+
export interface RecentConversation {
|
|
9
|
+
id: string;
|
|
10
|
+
mtime: number;
|
|
11
|
+
dateStr: string;
|
|
12
|
+
firstPrompt: string;
|
|
13
|
+
totalSteps: number;
|
|
14
|
+
userTurns: number;
|
|
15
|
+
}
|
|
16
|
+
export interface ConversationStepItem {
|
|
17
|
+
index: number;
|
|
18
|
+
role: "user" | "assistant" | "tool";
|
|
19
|
+
text: string;
|
|
20
|
+
}
|
|
21
|
+
export declare function listRecentConversations(limit?: number): RecentConversation[];
|
|
22
|
+
export declare function getConversationSteps(cid: string): {
|
|
23
|
+
totalSteps: number;
|
|
24
|
+
userTurns: number;
|
|
25
|
+
steps: ConversationStepItem[];
|
|
26
|
+
} | null;
|
|
27
|
+
export declare function formatStepsTimeline(steps: ConversationStepItem[]): string;
|
|
28
|
+
export declare function resolveTargetConversationId(targetId: string): string | null;
|
|
29
|
+
export declare function formatUsageOutput(rawText: string): string;
|
|
30
|
+
export declare function executeSlashCommand(promptText: string, session: Session, binaryPath: string): Promise<{
|
|
31
|
+
handled: boolean;
|
|
32
|
+
response?: string;
|
|
33
|
+
}>;
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { execFile } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
import { logger } from "./logger.js";
|
|
7
|
+
import { sessionStore } from "./session-store.js";
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const AGY_COMMAND_TIMEOUT_MS = 30_000;
|
|
10
|
+
const AGY_COMMAND_MAX_BUFFER = 4 * 1024 * 1024;
|
|
11
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12
|
+
const SAFE_LOOKUP_ID_RE = /^[A-Za-z0-9_-]{6,128}$/;
|
|
13
|
+
const SAFE_CONVERSATION_PREFIX_RE = /^[0-9a-f-]{6,36}$/i;
|
|
14
|
+
export const AVAILABLE_SLASH_COMMANDS = [
|
|
15
|
+
{ name: "resume", description: "Listar ou retomar uma sessão anterior (/resume ou /resume <id>)", argumentHint: "<conversation-ou-agent-id>" },
|
|
16
|
+
{ name: "usage", description: "Exibir uso e quota restante dos modelos (Gemini, Claude, GPT)" },
|
|
17
|
+
{ name: "quota", description: "Alias para /usage" },
|
|
18
|
+
{ name: "help", description: "Exibir lista de comandos e opções disponíveis" },
|
|
19
|
+
{ name: "credits", description: "Exibir saldo de créditos G1" },
|
|
20
|
+
{ name: "skills", description: "Listar skills instaladas no Antigravity" },
|
|
21
|
+
{ name: "agents", description: "Listar agentes disponíveis" },
|
|
22
|
+
{ name: "changelog", description: "Exibir notas de versão e novidades do Antigravity" },
|
|
23
|
+
];
|
|
24
|
+
function brainDir() {
|
|
25
|
+
return path.join(os.homedir(), ".gemini", "antigravity-cli", "brain");
|
|
26
|
+
}
|
|
27
|
+
function transcriptPath(conversationId) {
|
|
28
|
+
if (!UUID_RE.test(conversationId))
|
|
29
|
+
return null;
|
|
30
|
+
return path.join(brainDir(), conversationId, ".system_generated", "logs", "transcript.jsonl");
|
|
31
|
+
}
|
|
32
|
+
function summarizeConversation(cid, logFile, mtime) {
|
|
33
|
+
try {
|
|
34
|
+
const lines = fs.readFileSync(logFile, "utf8").split("\n").filter(Boolean);
|
|
35
|
+
let firstPrompt = "";
|
|
36
|
+
let userTurns = 0;
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
if (!line.includes('"USER_INPUT"'))
|
|
39
|
+
continue;
|
|
40
|
+
userTurns++;
|
|
41
|
+
if (!firstPrompt) {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(line);
|
|
44
|
+
let text = String(parsed.content || "");
|
|
45
|
+
if (text.includes("<USER_REQUEST>")) {
|
|
46
|
+
text = text.split("<USER_REQUEST>")[1].split("</USER_REQUEST>")[0];
|
|
47
|
+
}
|
|
48
|
+
firstPrompt = text.trim().replace(/\s+/g, " ").slice(0, 80);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const date = new Date(mtime);
|
|
54
|
+
return {
|
|
55
|
+
id: cid,
|
|
56
|
+
mtime,
|
|
57
|
+
dateStr: date.toLocaleString("pt-BR", {
|
|
58
|
+
timeZone: "America/Sao_Paulo",
|
|
59
|
+
month: "2-digit",
|
|
60
|
+
day: "2-digit",
|
|
61
|
+
hour: "2-digit",
|
|
62
|
+
minute: "2-digit",
|
|
63
|
+
}),
|
|
64
|
+
firstPrompt: firstPrompt || "(sem texto inicial)",
|
|
65
|
+
totalSteps: lines.length,
|
|
66
|
+
userTurns,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export function listRecentConversations(limit = 10) {
|
|
74
|
+
const root = brainDir();
|
|
75
|
+
if (!fs.existsSync(root) || limit <= 0)
|
|
76
|
+
return [];
|
|
77
|
+
try {
|
|
78
|
+
// Stat every candidate first, then read only the newest transcripts. This
|
|
79
|
+
// keeps /resume proportional to the requested result count instead of the
|
|
80
|
+
// total accumulated history size.
|
|
81
|
+
const candidates = [];
|
|
82
|
+
for (const cid of fs.readdirSync(root)) {
|
|
83
|
+
if (!UUID_RE.test(cid))
|
|
84
|
+
continue;
|
|
85
|
+
const logFile = transcriptPath(cid);
|
|
86
|
+
if (!logFile || !fs.existsSync(logFile))
|
|
87
|
+
continue;
|
|
88
|
+
try {
|
|
89
|
+
candidates.push({ id: cid, logFile, mtime: fs.statSync(logFile).mtimeMs });
|
|
90
|
+
}
|
|
91
|
+
catch { }
|
|
92
|
+
}
|
|
93
|
+
candidates.sort((a, b) => b.mtime - a.mtime);
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const candidate of candidates.slice(0, limit)) {
|
|
96
|
+
const summary = summarizeConversation(candidate.id, candidate.logFile, candidate.mtime);
|
|
97
|
+
if (summary)
|
|
98
|
+
results.push(summary);
|
|
99
|
+
}
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
logger.warn("Failed to read recent conversations from brain", {
|
|
104
|
+
error: err.message,
|
|
105
|
+
});
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function getConversationSteps(cid) {
|
|
110
|
+
const logFile = transcriptPath(cid);
|
|
111
|
+
if (!logFile || !fs.existsSync(logFile))
|
|
112
|
+
return null;
|
|
113
|
+
try {
|
|
114
|
+
const lines = fs.readFileSync(logFile, "utf8").split("\n").filter(Boolean);
|
|
115
|
+
const steps = [];
|
|
116
|
+
let userTurns = 0;
|
|
117
|
+
for (let i = 0; i < lines.length; i++) {
|
|
118
|
+
try {
|
|
119
|
+
const data = JSON.parse(lines[i]);
|
|
120
|
+
const idx = typeof data.step_index === "number" ? data.step_index : i;
|
|
121
|
+
const type = data.type;
|
|
122
|
+
let text = "";
|
|
123
|
+
let role = "assistant";
|
|
124
|
+
if (type === "USER_INPUT") {
|
|
125
|
+
userTurns++;
|
|
126
|
+
role = "user";
|
|
127
|
+
let raw = String(data.content || "");
|
|
128
|
+
if (raw.includes("<USER_REQUEST>")) {
|
|
129
|
+
raw = raw.split("<USER_REQUEST>")[1].split("</USER_REQUEST>")[0];
|
|
130
|
+
}
|
|
131
|
+
text = raw.trim().replace(/\s+/g, " ").slice(0, 90);
|
|
132
|
+
}
|
|
133
|
+
else if (type === "PLANNER_RESPONSE") {
|
|
134
|
+
const toolCalls = Array.isArray(data.tool_calls) ? data.tool_calls : [];
|
|
135
|
+
if (toolCalls.length > 0) {
|
|
136
|
+
role = "tool";
|
|
137
|
+
const toolNames = toolCalls.map((tc) => {
|
|
138
|
+
const name = tc.name || tc.tool_name || "tool";
|
|
139
|
+
let target = "";
|
|
140
|
+
if (tc.args) {
|
|
141
|
+
if (typeof tc.args.AbsolutePath === "string") {
|
|
142
|
+
target = path.basename(tc.args.AbsolutePath.replace(/"/g, ""));
|
|
143
|
+
}
|
|
144
|
+
else if (typeof tc.args.CommandLine === "string") {
|
|
145
|
+
target = tc.args.CommandLine.slice(0, 25);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return target ? `${name}(${target})` : name;
|
|
149
|
+
});
|
|
150
|
+
text = "Ferramenta: " + toolNames.join(", ");
|
|
151
|
+
}
|
|
152
|
+
else if (data.content) {
|
|
153
|
+
text = String(data.content).trim().replace(/\s+/g, " ").slice(0, 90);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (text)
|
|
157
|
+
steps.push({ index: idx, role, text });
|
|
158
|
+
}
|
|
159
|
+
catch { }
|
|
160
|
+
}
|
|
161
|
+
return { totalSteps: lines.length, userTurns, steps };
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
export function formatStepsTimeline(steps) {
|
|
168
|
+
if (steps.length === 0)
|
|
169
|
+
return "";
|
|
170
|
+
let md = "#### 📜 Histórico de Passos (Steps):\n";
|
|
171
|
+
const append = (step) => {
|
|
172
|
+
const roleLabel = step.role === "user" ? "User" : step.role === "tool" ? "Ferramenta" : "Assistente";
|
|
173
|
+
md += `• **Step ${step.index}** *(${roleLabel})*: ${step.text}\n`;
|
|
174
|
+
};
|
|
175
|
+
if (steps.length <= 8) {
|
|
176
|
+
steps.forEach(append);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
steps.slice(0, 2).forEach(append);
|
|
180
|
+
md += `• *... (${steps.length - 7} passos intermediários ocultos) ...*\n`;
|
|
181
|
+
steps.slice(-5).forEach(append);
|
|
182
|
+
}
|
|
183
|
+
return md;
|
|
184
|
+
}
|
|
185
|
+
export function resolveTargetConversationId(targetId) {
|
|
186
|
+
const cleanId = targetId.trim();
|
|
187
|
+
if (!cleanId || !SAFE_LOOKUP_ID_RE.test(cleanId))
|
|
188
|
+
return null;
|
|
189
|
+
const persisted = sessionStore.load(cleanId);
|
|
190
|
+
if (persisted?.conversationId && UUID_RE.test(persisted.conversationId)) {
|
|
191
|
+
return persisted.conversationId;
|
|
192
|
+
}
|
|
193
|
+
const root = brainDir();
|
|
194
|
+
if (UUID_RE.test(cleanId) && fs.existsSync(path.join(root, cleanId)))
|
|
195
|
+
return cleanId;
|
|
196
|
+
if (SAFE_CONVERSATION_PREFIX_RE.test(cleanId)) {
|
|
197
|
+
try {
|
|
198
|
+
const matches = fs
|
|
199
|
+
.readdirSync(root)
|
|
200
|
+
.filter((entry) => UUID_RE.test(entry) && entry.startsWith(cleanId));
|
|
201
|
+
if (matches.length === 1)
|
|
202
|
+
return matches[0];
|
|
203
|
+
if (matches.length > 1)
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
catch { }
|
|
207
|
+
}
|
|
208
|
+
const paseoAgentsDir = path.join(os.homedir(), ".paseo", "agents");
|
|
209
|
+
if (!fs.existsSync(paseoAgentsDir))
|
|
210
|
+
return null;
|
|
211
|
+
try {
|
|
212
|
+
for (const sub of fs.readdirSync(paseoAgentsDir)) {
|
|
213
|
+
const fullSub = path.join(paseoAgentsDir, sub);
|
|
214
|
+
if (!fs.statSync(fullSub).isDirectory())
|
|
215
|
+
continue;
|
|
216
|
+
for (const file of fs.readdirSync(fullSub)) {
|
|
217
|
+
if (!file.startsWith(cleanId) || !file.endsWith(".json"))
|
|
218
|
+
continue;
|
|
219
|
+
const agentJson = JSON.parse(fs.readFileSync(path.join(fullSub, file), "utf8"));
|
|
220
|
+
const paseoSessionId = agentJson.runtimeInfo?.sessionId;
|
|
221
|
+
if (typeof paseoSessionId !== "string")
|
|
222
|
+
continue;
|
|
223
|
+
const state = sessionStore.load(paseoSessionId);
|
|
224
|
+
if (state?.conversationId && UUID_RE.test(state.conversationId)) {
|
|
225
|
+
return state.conversationId;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch { }
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
function renderProgressBar(percentage) {
|
|
234
|
+
const filled = Math.max(0, Math.min(10, Math.round(percentage / 10)));
|
|
235
|
+
return "█".repeat(filled) + "░".repeat(10 - filled);
|
|
236
|
+
}
|
|
237
|
+
function getStatusBadge(percentage) {
|
|
238
|
+
if (percentage >= 70)
|
|
239
|
+
return "🟢";
|
|
240
|
+
if (percentage >= 30)
|
|
241
|
+
return "🟡";
|
|
242
|
+
return "🔴";
|
|
243
|
+
}
|
|
244
|
+
function formatCountdown(isoStr) {
|
|
245
|
+
try {
|
|
246
|
+
const target = new Date(isoStr);
|
|
247
|
+
const diffMs = target.getTime() - Date.now();
|
|
248
|
+
if (!Number.isFinite(diffMs))
|
|
249
|
+
return isoStr;
|
|
250
|
+
if (diffMs <= 0)
|
|
251
|
+
return "agora";
|
|
252
|
+
const diffHours = Math.floor(diffMs / 3_600_000);
|
|
253
|
+
const diffMins = Math.floor((diffMs % 3_600_000) / 60_000);
|
|
254
|
+
const timeStr = target.toLocaleTimeString("pt-BR", {
|
|
255
|
+
timeZone: "America/Sao_Paulo",
|
|
256
|
+
hour: "2-digit",
|
|
257
|
+
minute: "2-digit",
|
|
258
|
+
});
|
|
259
|
+
const dateStr = target.toLocaleDateString("pt-BR", {
|
|
260
|
+
timeZone: "America/Sao_Paulo",
|
|
261
|
+
day: "2-digit",
|
|
262
|
+
month: "2-digit",
|
|
263
|
+
});
|
|
264
|
+
if (diffHours < 24)
|
|
265
|
+
return `em **${diffHours}h ${diffMins}m** *(às ${timeStr})*`;
|
|
266
|
+
const days = Math.floor(diffHours / 24);
|
|
267
|
+
return `em **${days} ${days === 1 ? "dia" : "dias"}** *(${dateStr} às ${timeStr})*`;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
return isoStr;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
export function formatUsageOutput(rawText) {
|
|
274
|
+
const groups = new Map();
|
|
275
|
+
for (const line of rawText.trim().split("\n")) {
|
|
276
|
+
const parts = line.includes("\t") ? line.split("\t") : line.split(/\s{2,}/);
|
|
277
|
+
if (parts.length < 3)
|
|
278
|
+
continue;
|
|
279
|
+
let fam = parts[0].trim();
|
|
280
|
+
let icon = "🤖";
|
|
281
|
+
if (fam.toLowerCase().includes("gemini")) {
|
|
282
|
+
fam = "Google Gemini";
|
|
283
|
+
icon = "🔷";
|
|
284
|
+
}
|
|
285
|
+
else if (fam.toLowerCase().includes("claude")) {
|
|
286
|
+
fam = "Claude & GPT";
|
|
287
|
+
icon = "🔶";
|
|
288
|
+
}
|
|
289
|
+
const rawWindow = parts[1].replace(" Remaining", "").trim();
|
|
290
|
+
const lowerWindow = rawWindow.toLowerCase();
|
|
291
|
+
const isFiveHour = lowerWindow.includes("five hour") || /\b5\s*hour/.test(lowerWindow);
|
|
292
|
+
const win = isFiveHour
|
|
293
|
+
? "Janela de 5 Horas"
|
|
294
|
+
: lowerWindow.includes("weekly")
|
|
295
|
+
? "Cota Semanal"
|
|
296
|
+
: rawWindow;
|
|
297
|
+
const pctStr = parts[2].trim();
|
|
298
|
+
const parsedPct = Number.parseInt(pctStr.replace("%", ""), 10);
|
|
299
|
+
const pct = Number.isFinite(parsedPct) ? parsedPct : 0;
|
|
300
|
+
const reset = parts[3]?.trim() || "";
|
|
301
|
+
if (!groups.has(fam))
|
|
302
|
+
groups.set(fam, { icon, items: [] });
|
|
303
|
+
groups.get(fam).items.push({ win, isFiveHour, pct, pctStr, reset });
|
|
304
|
+
}
|
|
305
|
+
if (groups.size === 0) {
|
|
306
|
+
return `### 📊 Quota de Uso do Antigravity\n\n\`\`\`text\n${rawText.trim()}\n\`\`\``;
|
|
307
|
+
}
|
|
308
|
+
let out = "### ⚡ Quotas de Uso — Antigravity\n\n";
|
|
309
|
+
for (const [fam, data] of groups.entries()) {
|
|
310
|
+
data.items.sort((a, b) => Number(b.isFiveHour) - Number(a.isFiveHour));
|
|
311
|
+
out += `**${data.icon} ${fam}**\n`;
|
|
312
|
+
for (const item of data.items) {
|
|
313
|
+
out += `• **${item.win}:** ${getStatusBadge(item.pct)} **${item.pctStr}** livre \n`;
|
|
314
|
+
out += ` \`[${renderProgressBar(item.pct)}]\` · Reset ${item.reset ? formatCountdown(item.reset) : ""}\n`;
|
|
315
|
+
}
|
|
316
|
+
out += "\n";
|
|
317
|
+
}
|
|
318
|
+
out += "---\n> 💡 *A janela de 5 horas renova dinamicamente conforme o tempo passa.*";
|
|
319
|
+
return out;
|
|
320
|
+
}
|
|
321
|
+
async function runAgySlash(binaryPath, cwd, slashCommand) {
|
|
322
|
+
const { stdout, stderr } = await execFileAsync(binaryPath, ["--print", slashCommand], {
|
|
323
|
+
cwd,
|
|
324
|
+
env: process.env,
|
|
325
|
+
timeout: AGY_COMMAND_TIMEOUT_MS,
|
|
326
|
+
maxBuffer: AGY_COMMAND_MAX_BUFFER,
|
|
327
|
+
});
|
|
328
|
+
return stdout.trim() || stderr.trim();
|
|
329
|
+
}
|
|
330
|
+
export async function executeSlashCommand(promptText, session, binaryPath) {
|
|
331
|
+
const trimmed = promptText.trim();
|
|
332
|
+
if (!trimmed.startsWith("/"))
|
|
333
|
+
return { handled: false };
|
|
334
|
+
const parts = trimmed.slice(1).split(/\s+/);
|
|
335
|
+
const command = parts[0].toLowerCase();
|
|
336
|
+
const args = parts.slice(1).join(" ").trim();
|
|
337
|
+
logger.info("Intercepted slash command", { command, args, sessionId: session.id });
|
|
338
|
+
switch (command) {
|
|
339
|
+
case "resume": {
|
|
340
|
+
if (!args) {
|
|
341
|
+
const recent = listRecentConversations(10);
|
|
342
|
+
if (recent.length === 0) {
|
|
343
|
+
return {
|
|
344
|
+
handled: true,
|
|
345
|
+
response: "Nenhuma sessão anterior foi encontrada no histórico local do Antigravity.",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
let list = "### 📋 Sessões Recentes do Antigravity\n\n";
|
|
349
|
+
for (const conversation of recent) {
|
|
350
|
+
const stepsText = conversation.totalSteps > 0
|
|
351
|
+
? ` · 🟢 **${conversation.totalSteps} steps** (${conversation.userTurns} turno${conversation.userTurns !== 1 ? "s" : ""})`
|
|
352
|
+
: "";
|
|
353
|
+
list += `• **${conversation.dateStr}**${stepsText} \n *${conversation.firstPrompt}* \n \`/resume ${conversation.id}\`\n\n`;
|
|
354
|
+
}
|
|
355
|
+
list += "---\n💡 *Copie ou digite `/resume <ID>` acima para retomar qualquer sessão.*";
|
|
356
|
+
return { handled: true, response: list };
|
|
357
|
+
}
|
|
358
|
+
const resolvedConvId = resolveTargetConversationId(args);
|
|
359
|
+
if (!resolvedConvId) {
|
|
360
|
+
return {
|
|
361
|
+
handled: true,
|
|
362
|
+
response: `❌ **Sessão não encontrada.**\n\nNão foi possível localizar o histórico para \`${args}\`.\n\nDigite \`/resume\` sem argumentos para ver a lista de sessões disponíveis.`,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
try {
|
|
366
|
+
// Validate --conversation immediately instead of reporting success and
|
|
367
|
+
// discovering a broken resume on the user's next message.
|
|
368
|
+
await session.resumeConversation(resolvedConvId);
|
|
369
|
+
}
|
|
370
|
+
catch (err) {
|
|
371
|
+
return {
|
|
372
|
+
handled: true,
|
|
373
|
+
response: `❌ **Falha ao retomar sessão.**\n\n${err.message}`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
logger.info("Resumed conversation for session", {
|
|
377
|
+
sessionId: session.id,
|
|
378
|
+
conversationId: resolvedConvId,
|
|
379
|
+
});
|
|
380
|
+
const details = getConversationSteps(resolvedConvId);
|
|
381
|
+
const stepsSection = details?.steps.length ? `\n${formatStepsTimeline(details.steps)}\n` : "";
|
|
382
|
+
const stepsInfo = details
|
|
383
|
+
? `- **Total de Steps:** ${details.totalSteps} steps (${details.userTurns} turno${details.userTurns !== 1 ? "s" : ""})\n`
|
|
384
|
+
: "";
|
|
385
|
+
return {
|
|
386
|
+
handled: true,
|
|
387
|
+
response: `✅ **Sessão retomada com sucesso!**\n\n- **Conversation ID:** \`${resolvedConvId}\`\n${stepsInfo}- **Modelo Ativo:** \`${session.model}\`\n${stepsSection}---\n💡 *Contexto carregado. Envie sua próxima mensagem para continuar a partir de onde parou!*`,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
case "usage":
|
|
391
|
+
case "quota":
|
|
392
|
+
try {
|
|
393
|
+
return { handled: true, response: formatUsageOutput(await runAgySlash(binaryPath, session.cwd, "/usage")) };
|
|
394
|
+
}
|
|
395
|
+
catch (err) {
|
|
396
|
+
return { handled: true, response: `Erro ao obter uso de quota: ${err.message}` };
|
|
397
|
+
}
|
|
398
|
+
case "credits":
|
|
399
|
+
try {
|
|
400
|
+
return { handled: true, response: `### 💳 Saldo de Créditos Antigravity\n\n\`\`\`text\n${await runAgySlash(binaryPath, session.cwd, "/credits")}\n\`\`\`` };
|
|
401
|
+
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
return { handled: true, response: `Erro ao consultar créditos: ${err.message}` };
|
|
404
|
+
}
|
|
405
|
+
case "skills":
|
|
406
|
+
try {
|
|
407
|
+
return { handled: true, response: `### 🛠️ Skills Instaladas\n\n\`\`\`text\n${await runAgySlash(binaryPath, session.cwd, "/skills")}\n\`\`\`` };
|
|
408
|
+
}
|
|
409
|
+
catch (err) {
|
|
410
|
+
return { handled: true, response: `Erro ao listar skills: ${err.message}` };
|
|
411
|
+
}
|
|
412
|
+
case "agents":
|
|
413
|
+
try {
|
|
414
|
+
return { handled: true, response: `### 🤖 Agentes Disponíveis\n\n\`\`\`text\n${await runAgySlash(binaryPath, session.cwd, "/agents")}\n\`\`\`` };
|
|
415
|
+
}
|
|
416
|
+
catch (err) {
|
|
417
|
+
return { handled: true, response: `Erro ao listar agentes: ${err.message}` };
|
|
418
|
+
}
|
|
419
|
+
case "changelog":
|
|
420
|
+
try {
|
|
421
|
+
return { handled: true, response: `### 📜 Notas de Atualização (Changelog)\n\n${await runAgySlash(binaryPath, session.cwd, "/changelog")}` };
|
|
422
|
+
}
|
|
423
|
+
catch (err) {
|
|
424
|
+
return { handled: true, response: `Erro ao buscar changelog: ${err.message}` };
|
|
425
|
+
}
|
|
426
|
+
case "help":
|
|
427
|
+
return {
|
|
428
|
+
handled: true,
|
|
429
|
+
response: "### 📖 Comandos Disponíveis no Antigravity (Paseo)\n\n| Comando | Descrição |\n|:---|:---|\n| `/resume` | Lista as últimas 10 sessões disponíveis para retomada |\n| `/resume <id>` | Retoma e valida a sessão especificada |\n| `/usage` | Exibe o consumo e cotas restantes (Gemini, Claude, GPT) |\n| `/credits` | Exibe o saldo de créditos G1 |\n| `/skills` | Lista as skills instaladas no ambiente |\n| `/agents` | Lista os tipos de agentes configurados |\n| `/changelog` | Exibe as notas de versão do Antigravity |\n| `/help` | Exibe este painel de ajuda |\n",
|
|
430
|
+
};
|
|
431
|
+
default:
|
|
432
|
+
return { handled: false };
|
|
433
|
+
}
|
|
434
|
+
}
|