@agentex/agent 0.0.38 → 0.0.40

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.
Files changed (120) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/README.md +47 -13
  3. package/dist/history/fs.d.ts.map +1 -1
  4. package/dist/history/fs.js +6 -12
  5. package/dist/history/fs.js.map +1 -1
  6. package/dist/providers/antigravity/codec.d.ts +4 -0
  7. package/dist/providers/antigravity/codec.d.ts.map +1 -0
  8. package/dist/providers/antigravity/codec.js +27 -0
  9. package/dist/providers/antigravity/codec.js.map +1 -0
  10. package/dist/providers/antigravity/connection.d.ts +100 -0
  11. package/dist/providers/antigravity/connection.d.ts.map +1 -0
  12. package/dist/providers/antigravity/connection.js +267 -0
  13. package/dist/providers/antigravity/connection.js.map +1 -0
  14. package/dist/providers/antigravity/discovery.d.ts +11 -0
  15. package/dist/providers/antigravity/discovery.d.ts.map +1 -0
  16. package/dist/providers/antigravity/discovery.js +92 -0
  17. package/dist/providers/antigravity/discovery.js.map +1 -0
  18. package/dist/providers/antigravity/execute.d.ts +9 -0
  19. package/dist/providers/antigravity/execute.d.ts.map +1 -0
  20. package/dist/providers/antigravity/execute.js +206 -0
  21. package/dist/providers/antigravity/execute.js.map +1 -0
  22. package/dist/providers/antigravity/index.d.ts +12 -0
  23. package/dist/providers/antigravity/index.d.ts.map +1 -0
  24. package/dist/providers/antigravity/index.js +46 -0
  25. package/dist/providers/antigravity/index.js.map +1 -0
  26. package/dist/providers/antigravity/parse.d.ts +114 -0
  27. package/dist/providers/antigravity/parse.d.ts.map +1 -0
  28. package/dist/providers/antigravity/parse.js +369 -0
  29. package/dist/providers/antigravity/parse.js.map +1 -0
  30. package/dist/providers/antigravity/probe.d.ts +3 -0
  31. package/dist/providers/antigravity/probe.d.ts.map +1 -0
  32. package/dist/providers/antigravity/probe.js +84 -0
  33. package/dist/providers/antigravity/probe.js.map +1 -0
  34. package/dist/providers/antigravity/runtime.d.ts +30 -0
  35. package/dist/providers/antigravity/runtime.d.ts.map +1 -0
  36. package/dist/providers/antigravity/runtime.js +69 -0
  37. package/dist/providers/antigravity/runtime.js.map +1 -0
  38. package/dist/providers/antigravity/session.d.ts +7 -0
  39. package/dist/providers/antigravity/session.d.ts.map +1 -0
  40. package/dist/providers/antigravity/session.js +317 -0
  41. package/dist/providers/antigravity/session.js.map +1 -0
  42. package/dist/providers/claude/attach.d.ts.map +1 -1
  43. package/dist/providers/claude/attach.js +4 -6
  44. package/dist/providers/claude/attach.js.map +1 -1
  45. package/dist/providers/claude/transcript.d.ts.map +1 -1
  46. package/dist/providers/claude/transcript.js +9 -20
  47. package/dist/providers/claude/transcript.js.map +1 -1
  48. package/dist/providers/codex/attach.d.ts.map +1 -1
  49. package/dist/providers/codex/attach.js +4 -0
  50. package/dist/providers/codex/attach.js.map +1 -1
  51. package/dist/providers/codex/history.d.ts.map +1 -1
  52. package/dist/providers/codex/history.js +11 -0
  53. package/dist/providers/codex/history.js.map +1 -1
  54. package/dist/providers/codex/transcript-normalize.d.ts +12 -3
  55. package/dist/providers/codex/transcript-normalize.d.ts.map +1 -1
  56. package/dist/providers/codex/transcript-normalize.js +87 -10
  57. package/dist/providers/codex/transcript-normalize.js.map +1 -1
  58. package/dist/providers/codex/transcript.d.ts.map +1 -1
  59. package/dist/providers/codex/transcript.js +5 -10
  60. package/dist/providers/codex/transcript.js.map +1 -1
  61. package/dist/providers/codex/usage-scanner.d.ts.map +1 -1
  62. package/dist/providers/codex/usage-scanner.js +4 -6
  63. package/dist/providers/codex/usage-scanner.js.map +1 -1
  64. package/dist/providers/gemini/index.d.ts.map +1 -1
  65. package/dist/providers/gemini/index.js +6 -0
  66. package/dist/providers/gemini/index.js.map +1 -1
  67. package/dist/registry.d.ts.map +1 -1
  68. package/dist/registry.js +2 -0
  69. package/dist/registry.js.map +1 -1
  70. package/dist/types.d.ts +6 -5
  71. package/dist/types.d.ts.map +1 -1
  72. package/dist/types.js.map +1 -1
  73. package/dist/utils/auth.d.ts +1 -1
  74. package/dist/utils/auth.d.ts.map +1 -1
  75. package/dist/utils/auth.js +79 -1
  76. package/dist/utils/auth.js.map +1 -1
  77. package/dist/utils/binary.d.ts.map +1 -1
  78. package/dist/utils/binary.js +10 -0
  79. package/dist/utils/binary.js.map +1 -1
  80. package/dist/utils/instructions.d.ts.map +1 -1
  81. package/dist/utils/instructions.js +12 -9
  82. package/dist/utils/instructions.js.map +1 -1
  83. package/dist/utils/jsonl-lines.d.ts +35 -0
  84. package/dist/utils/jsonl-lines.d.ts.map +1 -0
  85. package/dist/utils/jsonl-lines.js +61 -0
  86. package/dist/utils/jsonl-lines.js.map +1 -0
  87. package/dist/utils/runtime-homes.d.ts.map +1 -1
  88. package/dist/utils/runtime-homes.js +4 -0
  89. package/dist/utils/runtime-homes.js.map +1 -1
  90. package/dist/utils/skills.d.ts +3 -3
  91. package/dist/utils/skills.d.ts.map +1 -1
  92. package/dist/utils/skills.js +16 -2
  93. package/dist/utils/skills.js.map +1 -1
  94. package/package.json +3 -2
  95. package/src/history/fs.ts +6 -12
  96. package/src/providers/antigravity/codec.ts +29 -0
  97. package/src/providers/antigravity/connection.ts +304 -0
  98. package/src/providers/antigravity/discovery.ts +90 -0
  99. package/src/providers/antigravity/execute.ts +214 -0
  100. package/src/providers/antigravity/index.ts +48 -0
  101. package/src/providers/antigravity/parse.ts +439 -0
  102. package/src/providers/antigravity/probe.ts +89 -0
  103. package/src/providers/antigravity/runtime.ts +64 -0
  104. package/src/providers/antigravity/session.ts +359 -0
  105. package/src/providers/claude/attach.ts +4 -6
  106. package/src/providers/claude/transcript.ts +9 -22
  107. package/src/providers/codex/attach.ts +4 -0
  108. package/src/providers/codex/history.ts +11 -0
  109. package/src/providers/codex/transcript-normalize.ts +85 -10
  110. package/src/providers/codex/transcript.ts +5 -11
  111. package/src/providers/codex/usage-scanner.ts +4 -7
  112. package/src/providers/gemini/index.ts +6 -0
  113. package/src/registry.ts +2 -0
  114. package/src/types.ts +6 -5
  115. package/src/utils/auth.ts +82 -1
  116. package/src/utils/binary.ts +10 -0
  117. package/src/utils/instructions.ts +10 -9
  118. package/src/utils/jsonl-lines.ts +80 -0
  119. package/src/utils/runtime-homes.ts +4 -0
  120. package/src/utils/skills.ts +17 -4
@@ -28,10 +28,10 @@ import { createReadStream } from "node:fs";
28
28
  import { readdir, stat, open as fsOpen } from "node:fs/promises";
29
29
  import * as os from "node:os";
30
30
  import * as path from "node:path";
31
- import * as readline from "node:readline";
32
31
 
33
32
  import { getDefaultRuntimeHome, getRuntimeHomeEnvVar } from "../../utils/runtime-homes.js";
34
33
  import type { FoundTranscript, TranscriptOps } from "../../types.js";
34
+ import { readJsonlLines } from "../../utils/jsonl-lines.js";
35
35
 
36
36
  /** Bytes scanned from the tail of the file in {@link peekCodexTranscript}. */
37
37
  const PEEK_TAIL_BYTES = 16 * 1024;
@@ -316,16 +316,11 @@ export async function* readCodexTranscript(
316
316
 
317
317
  const stream = createReadStream(filePath, { start: fromOffset, encoding: undefined });
318
318
  stream.on("error", () => {});
319
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
320
319
 
321
320
  const fileIdentity = rolloutIdentityFromPath(filePath);
322
- let pos = fromOffset;
323
321
  try {
324
- for await (const line of rl) {
325
- const lineStart = pos;
326
- pos += Buffer.byteLength(line, "utf8") + 1;
327
-
328
- const trimmed = line.trim();
322
+ for await (const line of readJsonlLines(stream, fromOffset)) {
323
+ const trimmed = line.text.trim();
329
324
  if (!trimmed) continue;
330
325
 
331
326
  const parsed = parseCodexLine(trimmed);
@@ -334,15 +329,14 @@ export async function* readCodexTranscript(
334
329
  // Replay-stable synthetic identity: (rollout identity, line start offset).
335
330
  // Codex emits no native per-event uuid, so this is the idempotency key
336
331
  // hosts use to dedup transcript replays. Deterministic across reads.
337
- parsed.eventId = `codex:${fileIdentity}:${lineStart}`;
332
+ parsed.eventId = `codex:${fileIdentity}:${line.start}`;
338
333
 
339
- yield { event: parsed, offset: pos };
334
+ yield { event: parsed, offset: line.end };
340
335
  }
341
336
  } catch (err) {
342
337
  const e = err as NodeJS.ErrnoException;
343
338
  if (e?.code !== "ENOENT") throw err;
344
339
  } finally {
345
- rl.close();
346
340
  stream.destroy();
347
341
  }
348
342
  }
@@ -1,9 +1,9 @@
1
1
  import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import * as os from "node:os";
4
- import * as readline from "node:readline";
5
4
  import { createReadStream } from "node:fs";
6
5
  import type { TokenUsage } from "../../types.js";
6
+ import { readJsonlLines } from "../../utils/jsonl-lines.js";
7
7
 
8
8
  // ---------------------------------------------------------------------------
9
9
  // Log path resolution
@@ -104,16 +104,14 @@ async function scanFile(
104
104
  ): Promise<void> {
105
105
  let stream: ReturnType<typeof createReadStream>;
106
106
  try {
107
- stream = createReadStream(filePath, { encoding: "utf-8" });
107
+ stream = createReadStream(filePath);
108
108
  } catch {
109
109
  return;
110
110
  }
111
111
 
112
- const rl = readline.createInterface({ input: stream, crlfDelay: Infinity });
113
-
114
112
  try {
115
- for await (const line of rl) {
116
- const trimmed = line.trim();
113
+ for await (const line of readJsonlLines(stream)) {
114
+ const trimmed = line.text.trim();
117
115
  if (!trimmed) continue;
118
116
 
119
117
  let parsed: Record<string, unknown>;
@@ -172,7 +170,6 @@ async function scanFile(
172
170
  usageByModel.set(model, existing);
173
171
  }
174
172
  } finally {
175
- rl.close();
176
173
  stream.destroy();
177
174
  }
178
175
  }
@@ -7,6 +7,12 @@ import { resolveAuthForProvider } from "../../utils/auth.js";
7
7
  * `@google/gemini-cli` on PATH; Gemini handles its own Google auth (GEMINI_API_KEY,
8
8
  * GOOGLE_API_KEY, or an OAuth login).
9
9
  *
10
+ * Since 2026-06-18 Gemini CLI no longer serves free, Google AI Pro, or Google
11
+ * AI Ultra sign-ins ("This client is no longer supported for Gemini Code
12
+ * Assist for individuals"); those accounts moved to the Antigravity CLI — use
13
+ * the `antigravity` provider. Gemini CLI still works with a paid API key and
14
+ * Gemini Code Assist Standard/Enterprise.
15
+ *
10
16
  * Replaces the previous one-shot `--output-format stream-json` adapter: the ACP
11
17
  * base gives gemini real sessions, streaming, tool-call correlation, permission
12
18
  * bridging (via `onUserInputRequest`), and mode discovery — none of which the
package/src/registry.ts CHANGED
@@ -8,6 +8,7 @@ import { cursorProvider } from "./providers/cursor/index.js";
8
8
  import { opencodeProvider } from "./providers/opencode/index.js";
9
9
  import { piProvider } from "./providers/pi/index.js";
10
10
  import { copilotProvider } from "./providers/copilot/index.js";
11
+ import { antigravityProvider } from "./providers/antigravity/index.js";
11
12
 
12
13
  const providers = new Map<string, ProviderModule>();
13
14
 
@@ -21,6 +22,7 @@ providers.set("cursor", cursorProvider);
21
22
  providers.set("opencode", opencodeProvider);
22
23
  providers.set("pi", piProvider);
23
24
  providers.set("copilot", copilotProvider);
25
+ providers.set("antigravity", antigravityProvider);
24
26
 
25
27
  export function getProvider(type: string): ProviderModule {
26
28
  const provider = providers.get(type);
package/src/types.ts CHANGED
@@ -667,8 +667,8 @@ export interface ProviderConfig {
667
667
  model?: string;
668
668
  /** Provider-native model variant, independent from reasoning effort. */
669
669
  modelVariant?: string;
670
- /** Provider reasoning effort. Claude maps this to `--effort`; Codex sends
671
- * it as the app-server `turn/start.effort` override. */
670
+ /** Provider reasoning effort. Claude and Antigravity map this to `--effort`;
671
+ * Codex sends it as the app-server `turn/start.effort` override. */
672
672
  effort?: string;
673
673
  /** Behavior when a host does not provide or loses its input callback. */
674
674
  unattendedPermissionPolicy?: "allow" | "deny";
@@ -729,15 +729,16 @@ export interface ProviderConfig {
729
729
  sandbox?: boolean;
730
730
  thinking?: string;
731
731
  /**
732
- * Provider-specific mode pass-through. Currently used by `cursor` for its
733
- * `--mode <mode>` flag. Don't use this for plan mode — set `planMode: true`
732
+ * Provider-specific mode pass-through. Used by `cursor` and `antigravity`
733
+ * for their `--mode <mode>` flag. Don't use this for plan mode — set `planMode: true`
734
734
  * instead, which is the cross-provider abstraction.
735
735
  */
736
736
  mode?: string;
737
737
  /**
738
738
  * Select a provider operating mode by id (one of `listModes()`). Honored by
739
739
  * providers with `capabilities.modes === true` (codex collaboration modes,
740
- * ACP session modes, copilot allow-all/agent/plan). Ignored otherwise.
740
+ * ACP session modes, copilot allow-all/agent/plan, antigravity
741
+ * default/accept-edits/plan). Ignored otherwise.
741
742
  * Distinct from `mode` (cursor's raw `--mode` passthrough) and `planMode`
742
743
  * (the cross-provider read-only abstraction).
743
744
  */
package/src/utils/auth.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import * as fs from "node:fs/promises";
2
+ import { readFileSync } from "node:fs";
2
3
  import * as path from "node:path";
3
4
  import * as os from "node:os";
4
5
  import type {
@@ -94,6 +95,12 @@ export function detectAuth(providerType: string, env: Record<string, string>): R
94
95
  }
95
96
  return { method: "subscription", billingType: "subscription" };
96
97
  }
98
+ case "antigravity": {
99
+ if (hasEnv(env, "GEMINI_API_KEY") && readAgyModelProvider() === "gemini") {
100
+ return { method: "api_key", billingType: "api" };
101
+ }
102
+ return { method: "subscription", billingType: "subscription" };
103
+ }
97
104
  case "opencode":
98
105
  case "pi":
99
106
  return { method: "api_key", billingType: "api" };
@@ -434,6 +441,74 @@ async function resolveGeminiAuth(ctx?: AuthResolveContext): Promise<AuthReport>
434
441
  };
435
442
  }
436
443
 
444
+ // ---------------------------------------------------------------------------
445
+ // Antigravity — no status subcommand, but `agy models` exits 1 with "Please
446
+ // sign in" when no cached sign-in exists, so its exit status is the CLI's own
447
+ // answer. A GEMINI_API_KEY counts only when settings.json selects the Gemini
448
+ // API (`"modelProvider": "gemini"`); the CLI ignores the variable otherwise.
449
+ // ---------------------------------------------------------------------------
450
+
451
+ /** Home directory as agy (a Go binary) resolves it: os.UserHomeDir semantics. */
452
+ function agyHomeDir(): string {
453
+ const fromEnv = process.platform === "win32" ? process.env["USERPROFILE"] : process.env["HOME"];
454
+ return fromEnv && fromEnv.trim() ? fromEnv : os.homedir();
455
+ }
456
+
457
+ function agySettingsPath(): string {
458
+ return path.join(agyHomeDir(), ".gemini", "antigravity-cli", "settings.json");
459
+ }
460
+
461
+ function readAgyModelProvider(): string | null {
462
+ try {
463
+ const settings = JSON.parse(readFileSync(agySettingsPath(), "utf-8")) as Record<string, unknown>;
464
+ return typeof settings["modelProvider"] === "string" ? settings["modelProvider"] : null;
465
+ } catch {
466
+ return null;
467
+ }
468
+ }
469
+
470
+ async function resolveAntigravityAuth(ctx?: AuthResolveContext): Promise<AuthReport> {
471
+ const env = buildEnv(ctx?.env);
472
+ const binary = await checkBinary("agy", ctx?.command, env);
473
+ const apiKeyProvider = readAgyModelProvider() === "gemini";
474
+ let signedIn = false;
475
+ let usedCli = false;
476
+ if (binary.installed && binary.resolvedPath) {
477
+ try {
478
+ const result = await runChildProcess({
479
+ runId: "antigravity-auth-status",
480
+ command: binary.resolvedPath,
481
+ args: [...binary.prefixArgs, "models"],
482
+ cwd: process.cwd(),
483
+ env,
484
+ timeoutSec: 20,
485
+ });
486
+ const output = `${result.stdout}\n${result.stderr}`;
487
+ signedIn = result.exitCode === 0 && !/sign in|authentication required/i.test(output);
488
+ usedCli = !result.timedOut;
489
+ } catch {
490
+ // Presence stays false; the binary could not be run.
491
+ }
492
+ }
493
+ return {
494
+ providerType: "antigravity",
495
+ binary: stripPrefixArgs(binary),
496
+ options: [
497
+ {
498
+ method: "subscription",
499
+ source: { kind: "cli", command: "agy models" },
500
+ present: signedIn && !apiKeyProvider,
501
+ },
502
+ {
503
+ method: "api_key",
504
+ source: { kind: "env", var: "GEMINI_API_KEY" },
505
+ present: apiKeyProvider && hasEnv(env, "GEMINI_API_KEY"),
506
+ },
507
+ ],
508
+ source: usedCli ? "cli" : "filesystem",
509
+ };
510
+ }
511
+
437
512
  // ---------------------------------------------------------------------------
438
513
  // Cursor uses its selected binary's `status` command. Other providers in this
439
514
  // section use environment or native-store presence reports.
@@ -538,6 +613,8 @@ async function resolveInternal(providerType: string, ctx?: AuthResolveContext):
538
613
  return resolveGeminiAuth(ctx);
539
614
  case "cursor":
540
615
  return resolveCursorAuth(ctx);
616
+ case "antigravity":
617
+ return resolveAntigravityAuth(ctx);
541
618
  case "opencode":
542
619
  return resolveOpencodeAuth(ctx);
543
620
  case "pi":
@@ -626,7 +703,7 @@ export async function hasBedrock(
626
703
  * Shell command a host should surface when an `auth_required` event fires
627
704
  * for `providerType`, or when `isLoggedIn` returns false. Values are the
628
705
  * provider's canonical CLI subcommand for interactive login — they spawn
629
- * a browser flow (Claude, Codex, Gemini, Cursor) or print API-key setup
706
+ * a browser flow (Claude, Codex, Gemini, Cursor, Antigravity) or print API-key setup
630
707
  * instructions (OpenCode, Pi).
631
708
  *
632
709
  * Unknown provider types fall back to `${providerType} login`, which is
@@ -647,6 +724,10 @@ export function loginCommandFor(providerType: string): string {
647
724
  return "gemini";
648
725
  case "cursor":
649
726
  return "cursor-agent login";
727
+ case "antigravity":
728
+ // No login subcommand: running `agy` signs in through the browser and
729
+ // caches the session in the OS keyring for headless runs.
730
+ return "agy";
650
731
  case "opencode":
651
732
  return "opencode auth login";
652
733
  case "pi":
@@ -44,6 +44,13 @@ const COMMON_PATHS_UNIX: Record<string, string[]> = {
44
44
  "/usr/local/bin/pi",
45
45
  path.join(os.homedir(), ".npm-global", "bin", "pi"),
46
46
  ],
47
+ // Antigravity CLI. Its installer writes ~/.local/bin/agy, which GUI apps
48
+ // launched outside a login shell usually don't have on PATH.
49
+ agy: [
50
+ path.join(os.homedir(), ".local", "bin", "agy"),
51
+ "/usr/local/bin/agy",
52
+ "/opt/homebrew/bin/agy",
53
+ ],
47
54
  };
48
55
 
49
56
  function getCommonPathsWindows(name: string): string[] {
@@ -54,6 +61,8 @@ function getCommonPathsWindows(name: string): string[] {
54
61
  path.join(appData, "npm", `${name}.cmd`),
55
62
  path.join(localAppData, "pnpm", `${name}.cmd`),
56
63
  path.join(userProfile, ".local", "bin", `${name}.exe`),
64
+ // Native installers that keep a per-tool folder (Antigravity: %LOCALAPPDATA%\agy\bin).
65
+ path.join(localAppData, name, "bin", `${name}.exe`),
57
66
  ];
58
67
  }
59
68
 
@@ -158,6 +167,7 @@ export async function findBinary(name: string, configOverride?: string): Promise
158
167
  agent: "Install the Cursor CLI agent",
159
168
  opencode: "Install: npm install -g opencode-ai",
160
169
  pi: "Install: npm install -g @mariozechner/pi-coding-agent",
170
+ agy: "Install the Antigravity CLI: curl -fsSL https://antigravity.google/cli/install.sh | bash",
161
171
  };
162
172
  throw new Error(
163
173
  `Could not find "${name}" binary. Searched common install paths and PATH. ` +
@@ -77,11 +77,12 @@ const RUNTIME_INSTRUCTIONS: Record<SkillRuntime, RuntimeInstructionSpec> = {
77
77
  codex: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: true },
78
78
  opencode: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: true },
79
79
  gemini: { projectFile: "AGENTS.md", nativeFile: "GEMINI.md", nativeFileMode: "copy", hasGlobalFile: true },
80
+ antigravity: { projectFile: "AGENTS.md", nativeFile: "GEMINI.md", nativeFileMode: "copy", hasGlobalFile: true },
80
81
  cursor: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: false },
81
82
  pi: { projectFile: "AGENTS.md", nativeFile: "AGENTS.md", nativeFileMode: "copy", hasGlobalFile: true },
82
83
  };
83
84
 
84
- const ALL_RUNTIMES: SkillRuntime[] = ["claude", "codex", "gemini", "cursor", "opencode", "pi"];
85
+ const ALL_RUNTIMES: SkillRuntime[] = ["claude", "codex", "gemini", "antigravity", "cursor", "opencode", "pi"];
85
86
 
86
87
  const DEFAULT_MANAGED_TAG = "agentex";
87
88
 
@@ -235,19 +236,19 @@ export function resolveInstructionTargets(options?: {
235
236
  return [...byFile.values()].map((target) => ({ ...target, runtimes: dedupeRuntimes(target.runtimes) }));
236
237
  }
237
238
 
238
- // global: each runtime reads its own native file in its own home dir.
239
- const targets: InstructionTarget[] = [];
239
+ // global: each runtime reads its own native file in its own home dir. Gemini
240
+ // CLI and Antigravity share ~/.gemini/GEMINI.md, so targets merge by path.
241
+ const byPath = new Map<string, InstructionTarget>();
240
242
  for (const runtime of runtimes) {
241
243
  const spec = RUNTIME_INSTRUCTIONS[runtime];
242
244
  if (!spec.hasGlobalFile) continue; // e.g. cursor global = app User Rules, not a file
243
245
  const home = getDefaultRuntimeHome(runtime, options?.homeDir);
244
- targets.push({
245
- filename: spec.nativeFile,
246
- targetPath: path.join(home, spec.nativeFile),
247
- runtimes: [runtime],
248
- });
246
+ const targetPath = path.join(home, spec.nativeFile);
247
+ const existing = byPath.get(targetPath);
248
+ if (existing) existing.runtimes.push(runtime);
249
+ else byPath.set(targetPath, { filename: spec.nativeFile, targetPath, runtimes: [runtime] });
249
250
  }
250
- return targets;
251
+ return [...byPath.values()];
251
252
  }
252
253
 
253
254
  function dedupeRuntimes(runtimes: SkillRuntime[]): SkillRuntime[] {
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Line splitting for JSONL files on disk.
3
+ *
4
+ * `node:readline` also breaks lines at U+2028 (LINE SEPARATOR), U+2029
5
+ * (PARAGRAPH SEPARATOR) and a lone `\r`. JSON allows U+2028/U+2029 unescaped
6
+ * inside strings, and both `JSON.stringify` (Claude Code) and serde_json
7
+ * (Codex) write them raw, typically in text copied from web pages. readline
8
+ * cuts such a record in two, both halves fail to parse, and the record
9
+ * silently disappears. Every byte offset after it also drifts by two bytes
10
+ * per separator, so a resume offset can land mid-record.
11
+ *
12
+ * {@link readJsonlLines} splits on `\n` only, drops the `\r` of a `\r\n`
13
+ * ending, and reports exact byte offsets.
14
+ */
15
+
16
+ export interface JsonlLine {
17
+ /** Line text without its `\n` or `\r\n` terminator. */
18
+ text: string;
19
+ /** Byte offset of the line's first byte. */
20
+ start: number;
21
+ /**
22
+ * Byte offset just past the line's `\n`, which is where the next line
23
+ * starts. For a final line with no `\n`, the end of the file.
24
+ */
25
+ end: number;
26
+ }
27
+
28
+ const NEWLINE = 0x0a;
29
+ const CARRIAGE_RETURN = 0x0d;
30
+
31
+ function decodeLine(bytes: Buffer): string {
32
+ const length = bytes.length > 0 && bytes[bytes.length - 1] === CARRIAGE_RETURN
33
+ ? bytes.length - 1
34
+ : bytes.length;
35
+ return bytes.toString("utf8", 0, length);
36
+ }
37
+
38
+ /**
39
+ * Yield the lines of a byte stream, typically `createReadStream(path, { start })`
40
+ * opened without an encoding. `startOffset` is the stream's starting byte
41
+ * offset in the file, so reported offsets are file offsets.
42
+ *
43
+ * Splitting happens on bytes before decoding. A `\n` byte never occurs inside
44
+ * a multi-byte UTF-8 sequence, so no character is split across lines.
45
+ */
46
+ export async function* readJsonlLines(
47
+ input: AsyncIterable<Buffer | string>,
48
+ startOffset = 0,
49
+ ): AsyncGenerator<JsonlLine> {
50
+ let pending: Buffer[] = [];
51
+ let pendingBytes = 0;
52
+ let lineStart = startOffset;
53
+
54
+ for await (const value of input) {
55
+ const chunk = typeof value === "string" ? Buffer.from(value, "utf8") : value;
56
+ let from = 0;
57
+ let newline = chunk.indexOf(NEWLINE, from);
58
+ while (newline !== -1) {
59
+ const piece = chunk.subarray(from, newline);
60
+ const bytes = pendingBytes > 0 ? Buffer.concat([...pending, piece], pendingBytes + piece.length) : piece;
61
+ const end = lineStart + bytes.length + 1;
62
+ yield { text: decodeLine(bytes), start: lineStart, end };
63
+ lineStart = end;
64
+ pending = [];
65
+ pendingBytes = 0;
66
+ from = newline + 1;
67
+ newline = chunk.indexOf(NEWLINE, from);
68
+ }
69
+ if (from < chunk.length) {
70
+ const tail = chunk.subarray(from);
71
+ pending.push(tail);
72
+ pendingBytes += tail.length;
73
+ }
74
+ }
75
+
76
+ if (pendingBytes > 0) {
77
+ const bytes = Buffer.concat(pending, pendingBytes);
78
+ yield { text: decodeLine(bytes), start: lineStart, end: lineStart + bytes.length };
79
+ }
80
+ }
@@ -10,6 +10,7 @@ const ENV_VAR_MAP: Record<SkillRuntime, string | null> = {
10
10
  claude: "CLAUDE_CONFIG_DIR",
11
11
  codex: "CODEX_HOME",
12
12
  gemini: "GEMINI_CONFIG_DIR",
13
+ antigravity: null, // agy has no home override
13
14
  cursor: "CURSOR_CONFIG_DIR",
14
15
  opencode: "XDG_CONFIG_HOME", // affects ~/.config/opencode/
15
16
  pi: "PI_HOME",
@@ -22,6 +23,9 @@ const HOME_SUBPATH_MAP: Record<SkillRuntime, string[]> = {
22
23
  claude: [".claude"],
23
24
  codex: [".codex"],
24
25
  gemini: [".gemini"],
26
+ // Shared with Gemini CLI: agy reads its global GEMINI.md from ~/.gemini and
27
+ // keeps its own settings and skills in ~/.gemini/antigravity-cli.
28
+ antigravity: [".gemini"],
25
29
  cursor: [".cursor"],
26
30
  opencode: [".config", "opencode"],
27
31
  pi: [".pi"],
@@ -2,13 +2,13 @@ import * as fs from "node:fs/promises";
2
2
  import * as path from "node:path";
3
3
  import * as os from "node:os";
4
4
 
5
- export type SkillRuntime = "claude" | "codex" | "gemini" | "cursor" | "opencode" | "pi";
5
+ export type SkillRuntime = "claude" | "codex" | "gemini" | "antigravity" | "cursor" | "opencode" | "pi";
6
6
 
7
7
  export type SkillLocation = "global" | "workspace";
8
8
 
9
9
  /**
10
10
  * The two standard skill discovery channels:
11
- * - "agents": ~/.agents/skills/ or {cwd}/.agents/skills/ — scanned by Codex, Gemini, Cursor, OpenCode, Pi
11
+ * - "agents": ~/.agents/skills/ or {cwd}/.agents/skills/ — scanned by Codex, Gemini, Antigravity, Cursor, OpenCode, Pi
12
12
  * - "claude": ~/.claude/skills/ or {cwd}/.claude/skills/ — scanned by Claude Code (the only agent that doesn't scan .agents/)
13
13
  */
14
14
  export type SkillChannel = "agents" | "claude";
@@ -23,6 +23,12 @@ const NATIVE_DIRS: Record<string, { global: string; workspace: (cwd: string) =>
23
23
  global: path.join(os.homedir(), ".gemini", "skills"),
24
24
  workspace: (cwd) => path.join(cwd, ".gemini", "skills"),
25
25
  },
26
+ // Antigravity reads workspace skills from the standard .agents/skills
27
+ // channel; only its global folder is its own.
28
+ antigravity: {
29
+ global: path.join(os.homedir(), ".gemini", "antigravity-cli", "skills"),
30
+ workspace: (cwd) => path.join(cwd, ".agents", "skills"),
31
+ },
26
32
  cursor: {
27
33
  global: path.join(os.homedir(), ".cursor", "skills"),
28
34
  workspace: (cwd) => path.join(cwd, ".cursor", "skills"),
@@ -139,7 +145,14 @@ function resolveTargets(
139
145
  ): Array<{ target: string; skillsHome: string }> {
140
146
  const targets = resolveStandardPaths(location, cwd);
141
147
  if (includeNativeDirs) {
142
- targets.push(...resolveNativePaths(location, cwd));
148
+ // A runtime whose native folder is a standard channel (Antigravity's
149
+ // workspace .agents/skills) is already covered by that channel.
150
+ const seen = new Set(targets.map((t) => t.skillsHome));
151
+ for (const native of resolveNativePaths(location, cwd)) {
152
+ if (seen.has(native.skillsHome)) continue;
153
+ seen.add(native.skillsHome);
154
+ targets.push(native);
155
+ }
143
156
  }
144
157
  return targets;
145
158
  }
@@ -152,7 +165,7 @@ function resolveTargets(
152
165
  * Install skill directories into agent discovery paths via idempotent symlinks.
153
166
  *
154
167
  * By default, installs into the two standard channels:
155
- * - `.agents/skills/` — discovered by Codex, Gemini, Cursor, OpenCode, Pi
168
+ * - `.agents/skills/` — discovered by Codex, Gemini, Antigravity, Cursor, OpenCode, Pi
156
169
  * - `.claude/skills/` — discovered by Claude Code
157
170
  *
158
171
  * Set `includeNativeDirs: true` to also install into each runtime's