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.
@@ -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;
@@ -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, buildConfigOptionsForModel, extractPromptText, mapToolNameToKind, calculateUsageCostUsd, roundUsageCostUsd, getModelContextWindow, fetchAntigravityUsage, } from "./protocol.js";
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: buildConfigOptionsForModel(session.model, session.effort, availableModels),
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 = buildConfigOptionsForModel(session.model, session.effort, models);
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 = buildConfigOptionsForModel(session.model, session.effort, models);
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 function resolveDefaultAgyBinary(): string;
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
- export function resolveDefaultAgyBinary() {
10
- if (process.env.AGY_BIN_PATH)
11
- return process.env.AGY_BIN_PATH;
12
- const home = os.homedir();
13
- if (home) {
14
- if (process.platform === "win32") {
15
- const appData = process.env.APPDATA || path.join(home, "AppData", "Roaming");
16
- const localAppData = process.env.LOCALAPPDATA || path.join(home, "AppData", "Local");
17
- const programFiles = process.env.ProgramFiles || "C:\\Program Files";
18
- const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
19
- const candidates = [
20
- path.join(home, ".local", "bin", "agy.exe"),
21
- path.join(home, ".local", "bin", "agy.cmd"),
22
- path.join(home, ".local", "bin", "agy.bat"),
23
- path.join(appData, "npm", "agy.cmd"),
24
- path.join(appData, "npm", "agy.exe"),
25
- path.join(appData, "npm", "agy.bat"),
26
- path.join(localAppData, "npm", "agy.cmd"),
27
- path.join(localAppData, "npm", "agy.exe"),
28
- path.join(localAppData, "Programs", "antigravity", "agy.exe"),
29
- path.join(localAppData, "Programs", "Antigravity", "bin", "agy.exe"),
30
- path.join(localAppData, "Microsoft", "WindowsApps", "agy.exe"),
31
- path.join(programFiles, "Antigravity", "bin", "agy.exe"),
32
- path.join(programFilesX86, "Antigravity", "bin", "agy.exe"),
33
- ];
34
- for (const cand of candidates) {
35
- if (fs.existsSync(cand))
36
- return cand;
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
- try {
39
- const out = execFileSync("where.exe", ["agy"], {
40
- encoding: "utf-8",
41
- timeout: 2000,
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
- return "agy";
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: isWin,
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
+ }
@@ -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
  }