paseo-acp-agy 1.1.8 → 1.2.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/dist/acp-server.d.ts +2 -0
- package/dist/acp-server.js +66 -4
- package/dist/antigravity-process.d.ts +6 -1
- package/dist/antigravity-process.js +119 -45
- package/dist/asar.d.ts +28 -0
- package/dist/asar.js +171 -0
- package/dist/attachments.js +3 -0
- package/dist/index.js +176 -7
- package/dist/paseo-patcher.d.ts +13 -0
- package/dist/paseo-patcher.js +286 -54
- package/dist/permissions.d.ts +1 -0
- package/dist/permissions.js +12 -0
- package/dist/protocol.d.ts +51 -1
- package/dist/protocol.js +141 -7
- package/dist/session-store.d.ts +1 -0
- package/dist/session-store.js +3 -0
- package/dist/session.d.ts +3 -0
- package/dist/session.js +20 -2
- package/dist/slash-commands.js +4 -3
- package/dist/version.js +1 -1
- package/package.json +5 -4
package/dist/acp-server.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare class ACPServer {
|
|
|
5
5
|
private input;
|
|
6
6
|
private output;
|
|
7
7
|
private binaryPath;
|
|
8
|
+
private skipNarration;
|
|
8
9
|
private rl;
|
|
9
10
|
private isRunning;
|
|
10
11
|
constructor(options?: {
|
|
@@ -12,6 +13,7 @@ export declare class ACPServer {
|
|
|
12
13
|
output?: Writable;
|
|
13
14
|
sessionManager?: SessionManager;
|
|
14
15
|
binaryPath?: string;
|
|
16
|
+
skipNarration?: boolean;
|
|
15
17
|
});
|
|
16
18
|
start(): void;
|
|
17
19
|
private send;
|
package/dist/acp-server.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
import { logger } from "./logger.js";
|
|
3
|
-
import { ACP_METHODS, AVAILABLE_MODES, fetchAvailableModels,
|
|
3
|
+
import { ACP_METHODS, AVAILABLE_MODES, AVAILABLE_PERMISSIONS, fetchAvailableModels, buildConfigOptionsForSession, extractPromptText, mapToolNameToKind, extractImageMarkdownLink, isNarrationText, calculateUsageCostUsd, roundUsageCostUsd, getModelContextWindow, fetchAntigravityUsage, } from "./protocol.js";
|
|
4
4
|
import { SessionManager } from "./session.js";
|
|
5
5
|
import { executeSlashCommand, AVAILABLE_SLASH_COMMANDS } from "./slash-commands.js";
|
|
6
6
|
import { getShortVersion } from "./version.js";
|
|
@@ -21,12 +21,15 @@ export class ACPServer {
|
|
|
21
21
|
input;
|
|
22
22
|
output;
|
|
23
23
|
binaryPath;
|
|
24
|
+
skipNarration;
|
|
24
25
|
rl = null;
|
|
25
26
|
isRunning = false;
|
|
26
27
|
constructor(options = {}) {
|
|
27
28
|
this.input = options.input || process.stdin;
|
|
28
29
|
this.output = options.output || process.stdout;
|
|
29
30
|
this.binaryPath = options.binaryPath || resolveDefaultAgyBinary();
|
|
31
|
+
this.skipNarration =
|
|
32
|
+
options.skipNarration ?? (process.env.AGY_ACP_SKIP_NARRATION === "true");
|
|
30
33
|
this.sessionManager =
|
|
31
34
|
options.sessionManager || new SessionManager({ defaultBinaryPath: this.binaryPath });
|
|
32
35
|
}
|
|
@@ -112,7 +115,13 @@ export class ACPServer {
|
|
|
112
115
|
availableModels,
|
|
113
116
|
currentModelId: session.model,
|
|
114
117
|
},
|
|
115
|
-
configOptions:
|
|
118
|
+
configOptions: buildConfigOptionsForSession({
|
|
119
|
+
modelId: session.model,
|
|
120
|
+
currentEffort: session.effort,
|
|
121
|
+
currentMode: session.mode,
|
|
122
|
+
currentPermission: session.permission,
|
|
123
|
+
availableModels,
|
|
124
|
+
}),
|
|
116
125
|
};
|
|
117
126
|
}
|
|
118
127
|
requireSession(sessionId) {
|
|
@@ -329,11 +338,19 @@ export class ACPServer {
|
|
|
329
338
|
this.sendSuccess(id, { stopReason: "end_turn" });
|
|
330
339
|
break;
|
|
331
340
|
}
|
|
341
|
+
let hasSentNonNarration = false;
|
|
332
342
|
const onStepUpdate = (event) => {
|
|
333
343
|
const step = event.step_update;
|
|
334
344
|
if (!step)
|
|
335
345
|
return;
|
|
336
346
|
if (step.step_type === "agent_response" && step.text_delta) {
|
|
347
|
+
if (this.skipNarration && !hasSentNonNarration) {
|
|
348
|
+
if (isNarrationText(step.text_delta)) {
|
|
349
|
+
logger.debug("Skipping pure narration text chunk", { delta: step.text_delta });
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
hasSentNonNarration = true;
|
|
353
|
+
}
|
|
337
354
|
this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
|
|
338
355
|
sessionId: session.id,
|
|
339
356
|
update: {
|
|
@@ -343,6 +360,7 @@ export class ACPServer {
|
|
|
343
360
|
});
|
|
344
361
|
}
|
|
345
362
|
else if (step.step_type === "thought" && step.text_delta) {
|
|
363
|
+
hasSentNonNarration = true;
|
|
346
364
|
this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
|
|
347
365
|
sessionId: session.id,
|
|
348
366
|
update: {
|
|
@@ -352,6 +370,7 @@ export class ACPServer {
|
|
|
352
370
|
});
|
|
353
371
|
}
|
|
354
372
|
else if (step.step_type === "tool" && step.tool_info) {
|
|
373
|
+
hasSentNonNarration = true;
|
|
355
374
|
const toolCallId = `tool_${step.step_index}`;
|
|
356
375
|
const toolName = step.tool_name || step.tool_info.name || "tool";
|
|
357
376
|
const toolKind = mapToolNameToKind(toolName);
|
|
@@ -377,6 +396,16 @@ export class ACPServer {
|
|
|
377
396
|
rawOutput: step.tool_info.output ?? "",
|
|
378
397
|
},
|
|
379
398
|
});
|
|
399
|
+
const imageMarkdown = extractImageMarkdownLink(toolName, step.tool_info.output, step.tool_info.parameters);
|
|
400
|
+
if (imageMarkdown) {
|
|
401
|
+
this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
|
|
402
|
+
sessionId: session.id,
|
|
403
|
+
update: {
|
|
404
|
+
sessionUpdate: "agent_message_chunk",
|
|
405
|
+
content: { type: "text", text: `\n\n${imageMarkdown}\n\n` },
|
|
406
|
+
},
|
|
407
|
+
});
|
|
408
|
+
}
|
|
380
409
|
}
|
|
381
410
|
else if (step.state === "ERROR") {
|
|
382
411
|
this.sendNotification(ACP_METHODS.SESSION_UPDATE, {
|
|
@@ -496,6 +525,7 @@ export class ACPServer {
|
|
|
496
525
|
break;
|
|
497
526
|
}
|
|
498
527
|
case ACP_METHODS.SESSION_SET_MODE:
|
|
528
|
+
case ACP_METHODS.SESSION_SET_MODE_CAMEL:
|
|
499
529
|
case ACP_METHODS.SESSION_SET_MODE_ALIAS: {
|
|
500
530
|
const sessionId = String(params.sessionId || "");
|
|
501
531
|
const modeId = String(params.modeId || params.mode || "default");
|
|
@@ -516,6 +546,7 @@ export class ACPServer {
|
|
|
516
546
|
break;
|
|
517
547
|
}
|
|
518
548
|
case ACP_METHODS.SESSION_SET_MODEL:
|
|
549
|
+
case ACP_METHODS.SESSION_SET_MODEL_CAMEL:
|
|
519
550
|
case ACP_METHODS.SESSION_SET_MODEL_ALIAS: {
|
|
520
551
|
const sessionId = String(params.sessionId || "");
|
|
521
552
|
const session = this.requireSession(sessionId);
|
|
@@ -546,12 +577,19 @@ export class ACPServer {
|
|
|
546
577
|
if (parsedModel.effort)
|
|
547
578
|
session.setEffort(parsedModel.effort);
|
|
548
579
|
session.setModel(model.modelId);
|
|
549
|
-
const configOptions =
|
|
580
|
+
const configOptions = buildConfigOptionsForSession({
|
|
581
|
+
modelId: session.model,
|
|
582
|
+
currentEffort: session.effort,
|
|
583
|
+
currentMode: session.mode,
|
|
584
|
+
currentPermission: session.permission,
|
|
585
|
+
availableModels: models,
|
|
586
|
+
});
|
|
550
587
|
if (!isNotification)
|
|
551
588
|
this.sendSuccess(id, { configOptions });
|
|
552
589
|
break;
|
|
553
590
|
}
|
|
554
591
|
case ACP_METHODS.SESSION_SET_CONFIG_OPTION:
|
|
592
|
+
case ACP_METHODS.SESSION_SET_CONFIG_OPTION_CAMEL:
|
|
555
593
|
case ACP_METHODS.SESSION_SET_CONFIG_OPTION_ALIAS: {
|
|
556
594
|
const sessionId = String(params.sessionId || "");
|
|
557
595
|
const configId = String(params.configId || "");
|
|
@@ -584,12 +622,36 @@ export class ACPServer {
|
|
|
584
622
|
session.setModel(value);
|
|
585
623
|
logger.info("Updated model for session via config option", { sessionId, model: value });
|
|
586
624
|
}
|
|
625
|
+
else if (configId === "mode") {
|
|
626
|
+
if (!AVAILABLE_MODES.some((candidate) => candidate.id === value)) {
|
|
627
|
+
if (!isNotification)
|
|
628
|
+
this.sendError(id, -32602, `Unsupported mode: ${value}`);
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
session.setMode(value);
|
|
632
|
+
logger.info("Updated mode for session via config option", { sessionId, mode: value });
|
|
633
|
+
}
|
|
634
|
+
else if (configId === "permission") {
|
|
635
|
+
if (!AVAILABLE_PERMISSIONS.some((candidate) => candidate.id === value)) {
|
|
636
|
+
if (!isNotification)
|
|
637
|
+
this.sendError(id, -32602, `Unsupported permission: ${value}`);
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
session.setPermission(value);
|
|
641
|
+
logger.info("Updated permission for session via config option", { sessionId, permission: value });
|
|
642
|
+
}
|
|
587
643
|
else {
|
|
588
644
|
if (!isNotification)
|
|
589
645
|
this.sendError(id, -32602, `Unsupported config option: ${configId}`);
|
|
590
646
|
break;
|
|
591
647
|
}
|
|
592
|
-
const configOptions =
|
|
648
|
+
const configOptions = buildConfigOptionsForSession({
|
|
649
|
+
modelId: session.model,
|
|
650
|
+
currentEffort: session.effort,
|
|
651
|
+
currentMode: session.mode,
|
|
652
|
+
currentPermission: session.permission,
|
|
653
|
+
availableModels: models,
|
|
654
|
+
});
|
|
593
655
|
if (!isNotification)
|
|
594
656
|
this.sendSuccess(id, { configOptions });
|
|
595
657
|
break;
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { EventEmitter } from "node:events";
|
|
2
2
|
import { AgyInitEvent, AgyStepUpdateEvent, AgyResultEvent } from "./protocol.js";
|
|
3
3
|
import { PermissionSettings } from "./permissions.js";
|
|
4
|
-
export declare
|
|
4
|
+
export declare const BINARY_RESOLVE_TTL_MS: number;
|
|
5
|
+
export declare function clearBinaryResolutionCache(): void;
|
|
6
|
+
export declare function isWindowsBatchScript(filePath: string): boolean;
|
|
7
|
+
export declare function resolveDefaultAgyBinary(force?: boolean): string;
|
|
5
8
|
export interface AntigravityProcessOptions {
|
|
6
9
|
binaryPath?: string;
|
|
7
10
|
cwd?: string;
|
|
@@ -37,6 +40,8 @@ export declare class AntigravityProcess extends EventEmitter {
|
|
|
37
40
|
setModel(model: string): void;
|
|
38
41
|
setEffort(effort: string): void;
|
|
39
42
|
setMode(mode: string): void;
|
|
43
|
+
get currentPermissions(): PermissionSettings;
|
|
44
|
+
setPermissions(permissions: Partial<PermissionSettings>): void;
|
|
40
45
|
setConversationId(conversationId?: string): void;
|
|
41
46
|
private scheduleRestart;
|
|
42
47
|
private isProcessTreeAlive;
|
|
@@ -6,54 +6,120 @@ import path from "node:path";
|
|
|
6
6
|
import { logger } from "./logger.js";
|
|
7
7
|
import { getEffectiveEffortForModel, } from "./protocol.js";
|
|
8
8
|
import { buildAgyArgs, resolvePermissionSettings } from "./permissions.js";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
9
|
+
let cachedBinaryPath = null;
|
|
10
|
+
let lastBinaryResolveTime = 0;
|
|
11
|
+
export const BINARY_RESOLVE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours TTL
|
|
12
|
+
export function clearBinaryResolutionCache() {
|
|
13
|
+
cachedBinaryPath = null;
|
|
14
|
+
lastBinaryResolveTime = 0;
|
|
15
|
+
}
|
|
16
|
+
export function isWindowsBatchScript(filePath) {
|
|
17
|
+
return /\.(cmd|bat)$/i.test(filePath);
|
|
18
|
+
}
|
|
19
|
+
export function resolveDefaultAgyBinary(force = false) {
|
|
20
|
+
const now = Date.now();
|
|
21
|
+
if (!force &&
|
|
22
|
+
cachedBinaryPath &&
|
|
23
|
+
now - lastBinaryResolveTime < BINARY_RESOLVE_TTL_MS &&
|
|
24
|
+
(cachedBinaryPath === "agy" || fs.existsSync(cachedBinaryPath))) {
|
|
25
|
+
return cachedBinaryPath;
|
|
26
|
+
}
|
|
27
|
+
let resolved = "agy";
|
|
28
|
+
if (process.env.AGY_BIN_PATH) {
|
|
29
|
+
resolved = process.env.AGY_BIN_PATH;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const home = os.homedir();
|
|
33
|
+
if (home) {
|
|
34
|
+
if (process.platform === "win32") {
|
|
35
|
+
const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
|
|
36
|
+
const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
|
|
37
|
+
const programFiles = process.env.ProgramFiles || "C:\\Program Files";
|
|
38
|
+
const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
|
|
39
|
+
const exeCandidates = [
|
|
40
|
+
path.join(localAppData, "Programs", "Antigravity", "bin", "agy.exe"),
|
|
41
|
+
path.join(localAppData, "Programs", "antigravity", "agy.exe"),
|
|
42
|
+
path.join(localAppData, "Programs", "Antigravity", "agy.exe"),
|
|
43
|
+
path.join(programFiles, "Antigravity", "bin", "agy.exe"),
|
|
44
|
+
path.join(programFilesX86, "Antigravity", "bin", "agy.exe"),
|
|
45
|
+
path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
|
|
46
|
+
path.join(home, ".local", "bin", "agy.exe"),
|
|
47
|
+
path.join(appData, "npm", "agy.exe"),
|
|
48
|
+
path.join(localAppData, "npm", "agy.exe"),
|
|
49
|
+
];
|
|
50
|
+
for (const cand of exeCandidates) {
|
|
51
|
+
if (fs.existsSync(cand)) {
|
|
52
|
+
resolved = cand;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (resolved === "agy") {
|
|
57
|
+
for (const target of ["agy", "agy.exe"]) {
|
|
58
|
+
try {
|
|
59
|
+
const out = execFileSync("where.exe", [target], {
|
|
60
|
+
encoding: "utf-8",
|
|
61
|
+
timeout: 2000,
|
|
62
|
+
windowsHide: true,
|
|
63
|
+
}).trim();
|
|
64
|
+
const lines = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
65
|
+
for (const line of lines) {
|
|
66
|
+
if (/\.exe$/i.test(line) && fs.existsSync(line)) {
|
|
67
|
+
resolved = line;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
if (resolved !== "agy")
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
catch { }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (resolved === "agy") {
|
|
78
|
+
const batchCandidates = [
|
|
79
|
+
path.join(appData, "npm", "agy.cmd"),
|
|
80
|
+
path.join(localAppData, "npm", "agy.cmd"),
|
|
81
|
+
path.join(home, ".local", "bin", "agy.cmd"),
|
|
82
|
+
path.join(appData, "npm", "agy.bat"),
|
|
83
|
+
];
|
|
84
|
+
for (const cand of batchCandidates) {
|
|
85
|
+
if (fs.existsSync(cand)) {
|
|
86
|
+
resolved = cand;
|
|
87
|
+
break;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (resolved === "agy") {
|
|
91
|
+
for (const target of ["agy", "agy.cmd", "agy.bat"]) {
|
|
92
|
+
try {
|
|
93
|
+
const out = execFileSync("where.exe", [target], {
|
|
94
|
+
encoding: "utf-8",
|
|
95
|
+
timeout: 2000,
|
|
96
|
+
windowsHide: true,
|
|
97
|
+
}).trim();
|
|
98
|
+
const lines = out.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
if (isWindowsBatchScript(line) && fs.existsSync(line)) {
|
|
101
|
+
resolved = line;
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (resolved !== "agy")
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
37
112
|
}
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
windowsHide: true,
|
|
43
|
-
}).trim();
|
|
44
|
-
const first = out.split(/\r?\n/)[0]?.trim();
|
|
45
|
-
if (first && fs.existsSync(first))
|
|
46
|
-
return first;
|
|
113
|
+
else {
|
|
114
|
+
const localPath = path.join(home, ".local", "bin", "agy");
|
|
115
|
+
if (fs.existsSync(localPath))
|
|
116
|
+
resolved = localPath;
|
|
47
117
|
}
|
|
48
|
-
catch { }
|
|
49
|
-
}
|
|
50
|
-
else {
|
|
51
|
-
const localPath = path.join(home, ".local", "bin", "agy");
|
|
52
|
-
if (fs.existsSync(localPath))
|
|
53
|
-
return localPath;
|
|
54
118
|
}
|
|
55
119
|
}
|
|
56
|
-
|
|
120
|
+
cachedBinaryPath = resolved;
|
|
121
|
+
lastBinaryResolveTime = now;
|
|
122
|
+
return resolved;
|
|
57
123
|
}
|
|
58
124
|
export class AntigravityProcess extends EventEmitter {
|
|
59
125
|
child = null;
|
|
@@ -123,6 +189,13 @@ export class AntigravityProcess extends EventEmitter {
|
|
|
123
189
|
this.scheduleRestart();
|
|
124
190
|
}
|
|
125
191
|
}
|
|
192
|
+
get currentPermissions() {
|
|
193
|
+
return this.permissions;
|
|
194
|
+
}
|
|
195
|
+
setPermissions(permissions) {
|
|
196
|
+
this.permissions = { ...this.permissions, ...permissions };
|
|
197
|
+
this.scheduleRestart();
|
|
198
|
+
}
|
|
126
199
|
setConversationId(conversationId) {
|
|
127
200
|
if (this.conversationId !== conversationId) {
|
|
128
201
|
this.conversationId = conversationId;
|
|
@@ -282,12 +355,13 @@ export class AntigravityProcess extends EventEmitter {
|
|
|
282
355
|
effectiveEffort,
|
|
283
356
|
});
|
|
284
357
|
const isWin = process.platform === "win32";
|
|
358
|
+
const isBatch = isWin && isWindowsBatchScript(this.binaryPath);
|
|
285
359
|
const child = spawn(this.binaryPath, args, {
|
|
286
360
|
cwd: this.cwd,
|
|
287
361
|
env: this.env,
|
|
288
362
|
stdio: ["pipe", "pipe", "pipe"],
|
|
289
363
|
detached: !isWin,
|
|
290
|
-
shell:
|
|
364
|
+
shell: isBatch,
|
|
291
365
|
windowsHide: true,
|
|
292
366
|
});
|
|
293
367
|
this.child = child;
|
package/dist/asar.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface AsarNode {
|
|
2
|
+
files?: Record<string, AsarNode>;
|
|
3
|
+
size?: number;
|
|
4
|
+
offset?: string;
|
|
5
|
+
executable?: boolean;
|
|
6
|
+
unpacked?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Reads and parses the ASAR archive header and returns the root node and base offset for data.
|
|
10
|
+
*/
|
|
11
|
+
export declare function readArchiveHeader(archivePath: string): {
|
|
12
|
+
header: AsarNode;
|
|
13
|
+
dataBaseOffset: number;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Lists all file paths inside an ASAR archive.
|
|
17
|
+
*/
|
|
18
|
+
export declare function listPackage(archivePath: string): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Pure TypeScript, zero-dependency ASAR archive extractor.
|
|
21
|
+
* Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
|
|
22
|
+
*/
|
|
23
|
+
export declare function extractAll(archivePath: string, destDir: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Pure TypeScript, zero-dependency ASAR archive builder.
|
|
26
|
+
* Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
|
|
27
|
+
*/
|
|
28
|
+
export declare function createPackage(srcDir: string, destFile: string): Promise<void>;
|
package/dist/asar.js
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* Reads and parses the ASAR archive header and returns the root node and base offset for data.
|
|
5
|
+
*/
|
|
6
|
+
export function readArchiveHeader(archivePath) {
|
|
7
|
+
if (!fs.existsSync(archivePath)) {
|
|
8
|
+
throw new Error(`ASAR archive not found: ${archivePath}`);
|
|
9
|
+
}
|
|
10
|
+
const fd = fs.openSync(archivePath, "r");
|
|
11
|
+
try {
|
|
12
|
+
const sizeBuf = Buffer.alloc(8);
|
|
13
|
+
const bytesRead = fs.readSync(fd, sizeBuf, 0, 8, 0);
|
|
14
|
+
if (bytesRead < 8) {
|
|
15
|
+
throw new Error(`Invalid ASAR archive: file too small (${bytesRead} bytes)`);
|
|
16
|
+
}
|
|
17
|
+
const payloadSize = sizeBuf.readUInt32LE(0);
|
|
18
|
+
const headerSize = sizeBuf.readUInt32LE(4);
|
|
19
|
+
if (payloadSize !== 4 || headerSize <= 0) {
|
|
20
|
+
throw new Error(`Invalid ASAR archive header size: payloadSize=${payloadSize}, headerSize=${headerSize}`);
|
|
21
|
+
}
|
|
22
|
+
const headerPickleBuf = Buffer.alloc(headerSize);
|
|
23
|
+
const headerRead = fs.readSync(fd, headerPickleBuf, 0, headerSize, 8);
|
|
24
|
+
if (headerRead < headerSize) {
|
|
25
|
+
throw new Error(`Truncated ASAR header: expected ${headerSize} bytes, got ${headerRead}`);
|
|
26
|
+
}
|
|
27
|
+
const strLen = headerPickleBuf.readUInt32LE(4);
|
|
28
|
+
const jsonStr = headerPickleBuf.subarray(8, 8 + strLen).toString("utf8");
|
|
29
|
+
const header = JSON.parse(jsonStr);
|
|
30
|
+
const dataBaseOffset = 8 + headerSize;
|
|
31
|
+
return { header, dataBaseOffset };
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
fs.closeSync(fd);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Lists all file paths inside an ASAR archive.
|
|
39
|
+
*/
|
|
40
|
+
export function listPackage(archivePath) {
|
|
41
|
+
const { header } = readArchiveHeader(archivePath);
|
|
42
|
+
const result = [];
|
|
43
|
+
function walk(node, currentPrefix) {
|
|
44
|
+
if (node.files) {
|
|
45
|
+
for (const [name, child] of Object.entries(node.files)) {
|
|
46
|
+
const next = currentPrefix ? `${currentPrefix}/${name}` : `/${name}`;
|
|
47
|
+
walk(child, next);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
result.push(currentPrefix);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
walk(header, "");
|
|
55
|
+
return result;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Pure TypeScript, zero-dependency ASAR archive extractor.
|
|
59
|
+
* Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
|
|
60
|
+
*/
|
|
61
|
+
export function extractAll(archivePath, destDir) {
|
|
62
|
+
const { header, dataBaseOffset } = readArchiveHeader(archivePath);
|
|
63
|
+
const fd = fs.openSync(archivePath, "r");
|
|
64
|
+
try {
|
|
65
|
+
function extractNode(node, currentPath, relParts = []) {
|
|
66
|
+
if (node.files) {
|
|
67
|
+
fs.mkdirSync(currentPath, { recursive: true });
|
|
68
|
+
for (const [name, child] of Object.entries(node.files)) {
|
|
69
|
+
extractNode(child, path.join(currentPath, name), [...relParts, name]);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (node.size !== undefined && node.offset !== undefined) {
|
|
73
|
+
const fileOffset = dataBaseOffset + parseInt(node.offset, 10);
|
|
74
|
+
const fileBuf = Buffer.alloc(node.size);
|
|
75
|
+
if (node.size > 0) {
|
|
76
|
+
fs.readSync(fd, fileBuf, 0, node.size, fileOffset);
|
|
77
|
+
}
|
|
78
|
+
fs.mkdirSync(path.dirname(currentPath), { recursive: true });
|
|
79
|
+
fs.writeFileSync(currentPath, fileBuf, {
|
|
80
|
+
mode: node.executable ? 0o755 : 0o644,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
else if (node.unpacked) {
|
|
84
|
+
const unpackedSrc = path.join(archivePath + ".unpacked", ...relParts);
|
|
85
|
+
if (fs.existsSync(unpackedSrc)) {
|
|
86
|
+
fs.mkdirSync(path.dirname(currentPath), { recursive: true });
|
|
87
|
+
fs.copyFileSync(unpackedSrc, currentPath);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
extractNode(header, destDir);
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
fs.closeSync(fd);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Pure TypeScript, zero-dependency ASAR archive builder.
|
|
99
|
+
* Compatible with all Node.js versions (Node 18, 20, 22, 24+) across Windows, macOS, and Linux.
|
|
100
|
+
*/
|
|
101
|
+
export async function createPackage(srcDir, destFile) {
|
|
102
|
+
const files = [];
|
|
103
|
+
function walk(dir, rel = "") {
|
|
104
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
105
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
106
|
+
for (const e of entries) {
|
|
107
|
+
const entryRel = rel ? `${rel}/${e.name}` : e.name;
|
|
108
|
+
const full = path.join(dir, e.name);
|
|
109
|
+
if (e.isDirectory()) {
|
|
110
|
+
walk(full, entryRel);
|
|
111
|
+
}
|
|
112
|
+
else if (e.isFile()) {
|
|
113
|
+
const stat = fs.statSync(full);
|
|
114
|
+
const executable = Boolean(stat.mode & 0o111);
|
|
115
|
+
files.push({
|
|
116
|
+
relPath: entryRel,
|
|
117
|
+
fullPath: full,
|
|
118
|
+
size: stat.size,
|
|
119
|
+
executable: executable || undefined,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
walk(srcDir);
|
|
125
|
+
const header = { files: {} };
|
|
126
|
+
let currentOffset = 0;
|
|
127
|
+
for (const f of files) {
|
|
128
|
+
const parts = f.relPath.split("/");
|
|
129
|
+
let curr = header.files;
|
|
130
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
131
|
+
if (!curr[parts[i]]) {
|
|
132
|
+
curr[parts[i]] = { files: {} };
|
|
133
|
+
}
|
|
134
|
+
curr = curr[parts[i]].files;
|
|
135
|
+
}
|
|
136
|
+
const leaf = parts[parts.length - 1];
|
|
137
|
+
curr[leaf] = {
|
|
138
|
+
size: f.size,
|
|
139
|
+
offset: String(currentOffset),
|
|
140
|
+
executable: f.executable,
|
|
141
|
+
};
|
|
142
|
+
currentOffset += f.size;
|
|
143
|
+
}
|
|
144
|
+
const jsonBuf = Buffer.from(JSON.stringify(header), "utf8");
|
|
145
|
+
const alignedJsonLen = (jsonBuf.length + 3) & ~3;
|
|
146
|
+
const headerPayloadSize = 4 + alignedJsonLen;
|
|
147
|
+
// Header pickle: payloadSize (uint32LE) + stringLength (uint32LE) + json bytes (padded to multiple of 4)
|
|
148
|
+
const headerPickle = Buffer.alloc(4 + headerPayloadSize, 0);
|
|
149
|
+
headerPickle.writeUInt32LE(headerPayloadSize, 0);
|
|
150
|
+
headerPickle.writeUInt32LE(jsonBuf.length, 4);
|
|
151
|
+
jsonBuf.copy(headerPickle, 8);
|
|
152
|
+
// Size pickle: 4 (uint32LE payload size) + headerPickle.length (uint32LE)
|
|
153
|
+
const sizePickle = Buffer.alloc(8, 0);
|
|
154
|
+
sizePickle.writeUInt32LE(4, 0);
|
|
155
|
+
sizePickle.writeUInt32LE(headerPickle.length, 4);
|
|
156
|
+
fs.mkdirSync(path.dirname(destFile), { recursive: true });
|
|
157
|
+
const fd = fs.openSync(destFile, "w");
|
|
158
|
+
try {
|
|
159
|
+
fs.writeSync(fd, sizePickle);
|
|
160
|
+
fs.writeSync(fd, headerPickle);
|
|
161
|
+
for (const f of files) {
|
|
162
|
+
if (f.size > 0) {
|
|
163
|
+
const content = fs.readFileSync(f.fullPath);
|
|
164
|
+
fs.writeSync(fd, content);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
fs.closeSync(fd);
|
|
170
|
+
}
|
|
171
|
+
}
|
package/dist/attachments.js
CHANGED
|
@@ -7,6 +7,9 @@ const DEFAULT_MAX_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
|
|
7
7
|
function getStateRoot() {
|
|
8
8
|
if (process.env.AGY_ACP_STATE_DIR)
|
|
9
9
|
return process.env.AGY_ACP_STATE_DIR;
|
|
10
|
+
if (process.platform === "win32" && process.env.LOCALAPPDATA) {
|
|
11
|
+
return path.join(process.env.LOCALAPPDATA, "agy-acp");
|
|
12
|
+
}
|
|
10
13
|
const stateHome = process.env.XDG_STATE_HOME || path.join(os.homedir(), ".local", "state");
|
|
11
14
|
return path.join(stateHome, "agy-acp");
|
|
12
15
|
}
|